Skip to content
Merged

2023.3.1 #89059

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion homeassistant/components/dormakaba_dkey/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
11 changes: 8 additions & 3 deletions homeassistant/components/knx/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
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
Expand Down Expand Up @@ -549,9 +550,12 @@ async def async_step_knxkeys_tunnel_select(
),
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()

Expand Down Expand Up @@ -708,7 +712,8 @@ def _process_upload() -> tuple[Keyring | None, dict[str, str]]:
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)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/nuheat/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
11 changes: 8 additions & 3 deletions homeassistant/components/sensor/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,15 +271,20 @@ def device_class(self) -> SensorDeviceClass | None:
@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:
Expand Down
15 changes: 8 additions & 7 deletions homeassistant/components/tibber/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,18 @@ async def _close(event: Event) -> None:

try:
await tibber_connection.update_info()
if not tibber_connection.name:
raise ConfigEntryNotReady("Could not fetch Tibber data.")

except asyncio.TimeoutError as err:
raise ConfigEntryNotReady from err
except aiohttp.ClientError as err:
_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)

Expand Down
8 changes: 6 additions & 2 deletions homeassistant/components/tibber/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ async def async_step_user(
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(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/tibber/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"quality_scale": "silver",
"requirements": ["pyTibber==0.26.13"]
"requirements": ["pyTibber==0.27.0"]
}
17 changes: 14 additions & 3 deletions homeassistant/components/tibber/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from homeassistant.util import Throttle, dt as dt_util

Expand Down Expand Up @@ -559,6 +560,8 @@ def get_live_measurement(self) -> Any:
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__(
Expand All @@ -571,9 +574,17 @@ def __init__(self, hass: HomeAssistant, tibber_connection: tibber.Tibber) -> Non

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."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
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)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 6 additions & 5 deletions tests/components/knx/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
41 changes: 41 additions & 0 deletions tests/components/sensor/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
[
Expand Down