diff --git a/homeassistant/components/dormakaba_dkey/manifest.json b/homeassistant/components/dormakaba_dkey/manifest.json index b837cf8dfed..7a4f6b9d905 100644 --- a/homeassistant/components/dormakaba_dkey/manifest.json +++ b/homeassistant/components/dormakaba_dkey/manifest.json @@ -11,5 +11,5 @@ "documentation": "https://www.home-assistant.io/integrations/dormakaba_dkey", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["py-dormakaba-dkey==1.0.3"] + "requirements": ["py-dormakaba-dkey==1.0.4"] } diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9cd10bb4d0a..c09f2d501c6 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -20,5 +20,5 @@ "documentation": "https://www.home-assistant.io/integrations/frontend", "integration_type": "system", "quality_scale": "internal", - "requirements": ["home-assistant-frontend==20230301.0"] + "requirements": ["home-assistant-frontend==20230302.0"] } diff --git a/homeassistant/components/knx/config_flow.py b/homeassistant/components/knx/config_flow.py index ec79dbc5f9a..85e23cbe547 100644 --- a/homeassistant/components/knx/config_flow.py +++ b/homeassistant/components/knx/config_flow.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod from collections.abc import AsyncGenerator from pathlib import Path +import shutil from typing import Any, Final import voluptuous as vol @@ -549,9 +550,12 @@ class KNXCommonFlow(ABC, FlowHandler): ), None, ) + _tunnel_identifier = selected_tunnel_ia or self.new_entry_data.get( + CONF_HOST + ) + _tunnel_suffix = f" @ {_tunnel_identifier}" if _tunnel_identifier else "" self.new_title = ( - f"{'Secure ' if _if_user_id else ''}" - f"Tunneling @ {selected_tunnel_ia or self.new_entry_data[CONF_HOST]}" + f"{'Secure ' if _if_user_id else ''}Tunneling{_tunnel_suffix}" ) return self.finish_flow() @@ -708,7 +712,8 @@ class KNXCommonFlow(ABC, FlowHandler): else: dest_path = Path(self.hass.config.path(STORAGE_DIR, DOMAIN)) dest_path.mkdir(exist_ok=True) - file_path.rename(dest_path / DEFAULT_KNX_KEYRING_FILENAME) + dest_file = dest_path / DEFAULT_KNX_KEYRING_FILENAME + shutil.move(file_path, dest_file) return keyring, errors keyring, errors = await self.hass.async_add_executor_job(_process_upload) diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index 91b0a9eb194..cda1e9b02dd 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -12,5 +12,5 @@ "documentation": "https://www.home-assistant.io/integrations/nuheat", "iot_class": "cloud_polling", "loggers": ["nuheat"], - "requirements": ["nuheat==1.0.0"] + "requirements": ["nuheat==1.0.1"] } diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index fd86024fbdf..1812f41693d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -271,15 +271,20 @@ class SensorEntity(Entity): @property def _numeric_state_expected(self) -> bool: """Return true if the sensor must be numeric.""" + # Note: the order of the checks needs to be kept aligned + # with the checks in `state` property. + device_class = try_parse_enum(SensorDeviceClass, self.device_class) + if device_class in NON_NUMERIC_DEVICE_CLASSES: + return False if ( self.state_class is not None or self.native_unit_of_measurement is not None or self.suggested_display_precision is not None ): return True - # Sensors with custom device classes are not considered numeric - device_class = try_parse_enum(SensorDeviceClass, self.device_class) - return device_class not in {None, *NON_NUMERIC_DEVICE_CLASSES} + # Sensors with custom device classes will have the device class + # converted to None and are not considered numeric + return device_class is not None @property def options(self) -> list[str] | None: diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 4d9c0560682..6bd68e17c4d 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -53,17 +53,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await tibber_connection.update_info() - if not tibber_connection.name: - raise ConfigEntryNotReady("Could not fetch Tibber data.") - except asyncio.TimeoutError as err: - raise ConfigEntryNotReady from err - except aiohttp.ClientError as err: - _LOGGER.error("Error connecting to Tibber: %s ", err) - return False + except ( + asyncio.TimeoutError, + aiohttp.ClientError, + tibber.RetryableHttpException, + ) as err: + raise ConfigEntryNotReady("Unable to connect") from err except tibber.InvalidLogin as exp: _LOGGER.error("Failed to login. %s", exp) return False + except tibber.FatalHttpException: + return False await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py index d0adc0391ab..b5cb4486cc9 100644 --- a/homeassistant/components/tibber/config_flow.py +++ b/homeassistant/components/tibber/config_flow.py @@ -44,10 +44,14 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await tibber_connection.update_info() except asyncio.TimeoutError: errors[CONF_ACCESS_TOKEN] = "timeout" - except aiohttp.ClientError: - errors[CONF_ACCESS_TOKEN] = "cannot_connect" except tibber.InvalidLogin: errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + except ( + aiohttp.ClientError, + tibber.RetryableHttpException, + tibber.FatalHttpException, + ): + errors[CONF_ACCESS_TOKEN] = "cannot_connect" if errors: return self.async_show_form( diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 0e23729df72..e716192b8b4 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["tibber"], "quality_scale": "silver", - "requirements": ["pyTibber==0.26.13"] + "requirements": ["pyTibber==0.27.0"] } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 5f375ee22ed..7c563208720 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -44,6 +44,7 @@ from homeassistant.helpers.entity_registry import async_get as async_get_entity_ from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, + UpdateFailed, ) from homeassistant.util import Throttle, dt as dt_util @@ -559,6 +560,8 @@ class TibberRtDataCoordinator(DataUpdateCoordinator): class TibberDataCoordinator(DataUpdateCoordinator[None]): """Handle Tibber data and insert statistics.""" + config_entry: ConfigEntry + def __init__(self, hass: HomeAssistant, tibber_connection: tibber.Tibber) -> None: """Initialize the data handler.""" super().__init__( @@ -571,9 +574,17 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]): async def _async_update_data(self) -> None: """Update data via API.""" - await self._tibber_connection.fetch_consumption_data_active_homes() - await self._tibber_connection.fetch_production_data_active_homes() - await self._insert_statistics() + try: + await self._tibber_connection.fetch_consumption_data_active_homes() + await self._tibber_connection.fetch_production_data_active_homes() + await self._insert_statistics() + except tibber.RetryableHttpException as err: + raise UpdateFailed(f"Error communicating with API ({err.status})") from err + except tibber.FatalHttpException: + # Fatal error. Reload config entry to show correct error. + self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id) + ) async def _insert_statistics(self) -> None: """Insert Tibber statistics.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index 2edef39b7c1..1ec896a415f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ from .backports.enum import StrEnum APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9ea8763eb99..3a199853634 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,14 +23,14 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==1.0.6 home-assistant-bluetooth==1.9.3 -home-assistant-frontend==20230301.0 +home-assistant-frontend==20230302.0 home-assistant-intents==2023.2.28 httpx==0.23.3 ifaddr==0.1.7 janus==1.0.0 jinja2==3.1.2 lru-dict==1.1.8 -orjson==3.8.6 +orjson==3.8.7 paho-mqtt==1.6.1 pillow==9.4.0 pip>=21.0,<23.1 @@ -40,7 +40,7 @@ pyserial==3.5 python-slugify==4.0.1 pyudev==0.23.2 pyyaml==6.0 -requests==2.28.1 +requests==2.28.2 scapy==2.5.0 sqlalchemy==2.0.4 typing-extensions>=4.5.0,<5.0 diff --git a/pyproject.toml b/pyproject.toml index ef4253a43d6..1a81cc5f502 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.3.0" +version = "2023.3.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" @@ -44,11 +44,11 @@ dependencies = [ "cryptography==39.0.1", # pyOpenSSL 23.0.0 is required to work with cryptography 39+ "pyOpenSSL==23.0.0", - "orjson==3.8.6", + "orjson==3.8.7", "pip>=21.0,<23.1", "python-slugify==4.0.1", "pyyaml==6.0", - "requests==2.28.1", + "requests==2.28.2", "typing-extensions>=4.5.0,<5.0", "voluptuous==0.13.1", "voluptuous-serialize==2.6.0", diff --git a/requirements.txt b/requirements.txt index aa6e85d1520..68d6bceb244 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,11 +18,11 @@ lru-dict==1.1.8 PyJWT==2.5.0 cryptography==39.0.1 pyOpenSSL==23.0.0 -orjson==3.8.6 +orjson==3.8.7 pip>=21.0,<23.1 python-slugify==4.0.1 pyyaml==6.0 -requests==2.28.1 +requests==2.28.2 typing-extensions>=4.5.0,<5.0 voluptuous==0.13.1 voluptuous-serialize==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4ef30162684..d11e4d66026 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230301.0 +home-assistant-frontend==20230302.0 # homeassistant.components.conversation home-assistant-intents==2023.2.28 @@ -1225,7 +1225,7 @@ nsapi==3.0.5 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==1.0.0 +nuheat==1.0.1 # homeassistant.components.numato numato-gpio==0.10.0 @@ -1430,7 +1430,7 @@ py-canary==0.5.3 py-cpuinfo==8.0.0 # homeassistant.components.dormakaba_dkey -py-dormakaba-dkey==1.0.3 +py-dormakaba-dkey==1.0.4 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -1473,7 +1473,7 @@ pyRFXtrx==0.30.1 pySwitchmate==0.5.1 # homeassistant.components.tibber -pyTibber==0.26.13 +pyTibber==0.27.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bf10bcc52d6..22d6a64361e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230301.0 +home-assistant-frontend==20230302.0 # homeassistant.components.conversation home-assistant-intents==2023.2.28 @@ -903,7 +903,7 @@ notify-events==1.0.4 nsw-fuel-api-client==1.1.0 # homeassistant.components.nuheat -nuheat==1.0.0 +nuheat==1.0.1 # homeassistant.components.numato numato-gpio==0.10.0 @@ -1045,7 +1045,7 @@ py-canary==0.5.3 py-cpuinfo==8.0.0 # homeassistant.components.dormakaba_dkey -py-dormakaba-dkey==1.0.3 +py-dormakaba-dkey==1.0.4 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -1076,7 +1076,7 @@ pyMetno==0.9.0 pyRFXtrx==0.30.1 # homeassistant.components.tibber -pyTibber==0.26.13 +pyTibber==0.27.0 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/tests/components/knx/test_config_flow.py b/tests/components/knx/test_config_flow.py index 4ac6a366119..054d7844714 100644 --- a/tests/components/knx/test_config_flow.py +++ b/tests/components/knx/test_config_flow.py @@ -77,16 +77,17 @@ def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None): side_effect=side_effect, ), patch( "pathlib.Path.mkdir" - ) as mkdir_mock: - file_path_mock = Mock() - file_upload_mock.return_value.__enter__.return_value = file_path_mock + ) as mkdir_mock, patch( + "shutil.move" + ) as shutil_move_mock: + file_upload_mock.return_value.__enter__.return_value = Mock() yield return_value if side_effect: mkdir_mock.assert_not_called() - file_path_mock.rename.assert_not_called() + shutil_move_mock.assert_not_called() else: mkdir_mock.assert_called_once() - file_path_mock.rename.assert_called_once() + shutil_move_mock.assert_called_once() def _gateway_descriptor( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 7d96d51d5ca..8be15f1c7cd 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -205,6 +205,47 @@ async def test_datetime_conversion( assert state.state == test_timestamp.isoformat() +async def test_a_sensor_with_a_non_numeric_device_class( + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + enable_custom_integrations: None, +) -> None: + """Test that a sensor with a non numeric device class will be non numeric. + + A non numeric sensor with a valid device class should never be + handled as numeric because it has a device class. + """ + test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=timezone.utc) + test_local_timestamp = test_timestamp.astimezone( + dt_util.get_time_zone("Europe/Amsterdam") + ) + + platform = getattr(hass.components, "test.sensor") + platform.init(empty=True) + platform.ENTITIES["0"] = platform.MockSensor( + name="Test", + native_value=test_local_timestamp, + native_unit_of_measurement="", + device_class=SensorDeviceClass.TIMESTAMP, + ) + + platform.ENTITIES["1"] = platform.MockSensor( + name="Test", + native_value=test_local_timestamp, + state_class="", + device_class=SensorDeviceClass.TIMESTAMP, + ) + + assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(platform.ENTITIES["0"].entity_id) + assert state.state == test_timestamp.isoformat() + + state = hass.states.get(platform.ENTITIES["1"].entity_id) + assert state.state == test_timestamp.isoformat() + + @pytest.mark.parametrize( ("device_class", "state_value", "provides"), [