This commit is contained in:
Paulus Schoutsen 2023-03-02 15:53:50 -05:00 committed by GitHub
commit f0f12fd14a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 112 additions and 44 deletions

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/dormakaba_dkey", "documentation": "https://www.home-assistant.io/integrations/dormakaba_dkey",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["py-dormakaba-dkey==1.0.3"] "requirements": ["py-dormakaba-dkey==1.0.4"]
} }

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230301.0"] "requirements": ["home-assistant-frontend==20230302.0"]
} }

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
from pathlib import Path from pathlib import Path
import shutil
from typing import Any, Final from typing import Any, Final
import voluptuous as vol import voluptuous as vol
@ -549,9 +550,12 @@ class KNXCommonFlow(ABC, FlowHandler):
), ),
None, 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 = ( self.new_title = (
f"{'Secure ' if _if_user_id else ''}" f"{'Secure ' if _if_user_id else ''}Tunneling{_tunnel_suffix}"
f"Tunneling @ {selected_tunnel_ia or self.new_entry_data[CONF_HOST]}"
) )
return self.finish_flow() return self.finish_flow()
@ -708,7 +712,8 @@ class KNXCommonFlow(ABC, FlowHandler):
else: else:
dest_path = Path(self.hass.config.path(STORAGE_DIR, DOMAIN)) dest_path = Path(self.hass.config.path(STORAGE_DIR, DOMAIN))
dest_path.mkdir(exist_ok=True) 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 return keyring, errors
keyring, errors = await self.hass.async_add_executor_job(_process_upload) keyring, errors = await self.hass.async_add_executor_job(_process_upload)

View File

@ -12,5 +12,5 @@
"documentation": "https://www.home-assistant.io/integrations/nuheat", "documentation": "https://www.home-assistant.io/integrations/nuheat",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["nuheat"], "loggers": ["nuheat"],
"requirements": ["nuheat==1.0.0"] "requirements": ["nuheat==1.0.1"]
} }

View File

@ -271,15 +271,20 @@ class SensorEntity(Entity):
@property @property
def _numeric_state_expected(self) -> bool: def _numeric_state_expected(self) -> bool:
"""Return true if the sensor must be numeric.""" """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 ( if (
self.state_class is not None self.state_class is not None
or self.native_unit_of_measurement is not None or self.native_unit_of_measurement is not None
or self.suggested_display_precision is not None or self.suggested_display_precision is not None
): ):
return True return True
# Sensors with custom device classes are not considered numeric # Sensors with custom device classes will have the device class
device_class = try_parse_enum(SensorDeviceClass, self.device_class) # converted to None and are not considered numeric
return device_class not in {None, *NON_NUMERIC_DEVICE_CLASSES} return device_class is not None
@property @property
def options(self) -> list[str] | None: def options(self) -> list[str] | None:

View File

@ -53,17 +53,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try: try:
await tibber_connection.update_info() await tibber_connection.update_info()
if not tibber_connection.name:
raise ConfigEntryNotReady("Could not fetch Tibber data.")
except asyncio.TimeoutError as err: except (
raise ConfigEntryNotReady from err asyncio.TimeoutError,
except aiohttp.ClientError as err: aiohttp.ClientError,
_LOGGER.error("Error connecting to Tibber: %s ", err) tibber.RetryableHttpException,
return False ) as err:
raise ConfigEntryNotReady("Unable to connect") from err
except tibber.InvalidLogin as exp: except tibber.InvalidLogin as exp:
_LOGGER.error("Failed to login. %s", exp) _LOGGER.error("Failed to login. %s", exp)
return False return False
except tibber.FatalHttpException:
return False
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@ -44,10 +44,14 @@ class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await tibber_connection.update_info() await tibber_connection.update_info()
except asyncio.TimeoutError: except asyncio.TimeoutError:
errors[CONF_ACCESS_TOKEN] = "timeout" errors[CONF_ACCESS_TOKEN] = "timeout"
except aiohttp.ClientError:
errors[CONF_ACCESS_TOKEN] = "cannot_connect"
except tibber.InvalidLogin: except tibber.InvalidLogin:
errors[CONF_ACCESS_TOKEN] = "invalid_access_token" errors[CONF_ACCESS_TOKEN] = "invalid_access_token"
except (
aiohttp.ClientError,
tibber.RetryableHttpException,
tibber.FatalHttpException,
):
errors[CONF_ACCESS_TOKEN] = "cannot_connect"
if errors: if errors:
return self.async_show_form( return self.async_show_form(

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["tibber"], "loggers": ["tibber"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["pyTibber==0.26.13"] "requirements": ["pyTibber==0.27.0"]
} }

View File

@ -44,6 +44,7 @@ from homeassistant.helpers.entity_registry import async_get as async_get_entity_
from homeassistant.helpers.update_coordinator import ( from homeassistant.helpers.update_coordinator import (
CoordinatorEntity, CoordinatorEntity,
DataUpdateCoordinator, DataUpdateCoordinator,
UpdateFailed,
) )
from homeassistant.util import Throttle, dt as dt_util from homeassistant.util import Throttle, dt as dt_util
@ -559,6 +560,8 @@ class TibberRtDataCoordinator(DataUpdateCoordinator):
class TibberDataCoordinator(DataUpdateCoordinator[None]): class TibberDataCoordinator(DataUpdateCoordinator[None]):
"""Handle Tibber data and insert statistics.""" """Handle Tibber data and insert statistics."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, tibber_connection: tibber.Tibber) -> None: def __init__(self, hass: HomeAssistant, tibber_connection: tibber.Tibber) -> None:
"""Initialize the data handler.""" """Initialize the data handler."""
super().__init__( super().__init__(
@ -571,9 +574,17 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Update data via API.""" """Update data via API."""
try:
await self._tibber_connection.fetch_consumption_data_active_homes() await self._tibber_connection.fetch_consumption_data_active_homes()
await self._tibber_connection.fetch_production_data_active_homes() await self._tibber_connection.fetch_production_data_active_homes()
await self._insert_statistics() 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: async def _insert_statistics(self) -> None:
"""Insert Tibber statistics.""" """Insert Tibber statistics."""

View File

@ -8,7 +8,7 @@ from .backports.enum import StrEnum
APPLICATION_NAME: Final = "HomeAssistant" APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2023 MAJOR_VERSION: Final = 2023
MINOR_VERSION: Final = 3 MINOR_VERSION: Final = 3
PATCH_VERSION: Final = "0" PATCH_VERSION: Final = "1"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0) REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 10, 0)

View File

@ -23,14 +23,14 @@ fnvhash==0.1.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0
hassil==1.0.6 hassil==1.0.6
home-assistant-bluetooth==1.9.3 home-assistant-bluetooth==1.9.3
home-assistant-frontend==20230301.0 home-assistant-frontend==20230302.0
home-assistant-intents==2023.2.28 home-assistant-intents==2023.2.28
httpx==0.23.3 httpx==0.23.3
ifaddr==0.1.7 ifaddr==0.1.7
janus==1.0.0 janus==1.0.0
jinja2==3.1.2 jinja2==3.1.2
lru-dict==1.1.8 lru-dict==1.1.8
orjson==3.8.6 orjson==3.8.7
paho-mqtt==1.6.1 paho-mqtt==1.6.1
pillow==9.4.0 pillow==9.4.0
pip>=21.0,<23.1 pip>=21.0,<23.1
@ -40,7 +40,7 @@ pyserial==3.5
python-slugify==4.0.1 python-slugify==4.0.1
pyudev==0.23.2 pyudev==0.23.2
pyyaml==6.0 pyyaml==6.0
requests==2.28.1 requests==2.28.2
scapy==2.5.0 scapy==2.5.0
sqlalchemy==2.0.4 sqlalchemy==2.0.4
typing-extensions>=4.5.0,<5.0 typing-extensions>=4.5.0,<5.0

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.3.0" version = "2023.3.1"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "Open-source home automation platform running on Python 3." description = "Open-source home automation platform running on Python 3."
readme = "README.rst" readme = "README.rst"
@ -44,11 +44,11 @@ dependencies = [
"cryptography==39.0.1", "cryptography==39.0.1",
# pyOpenSSL 23.0.0 is required to work with cryptography 39+ # pyOpenSSL 23.0.0 is required to work with cryptography 39+
"pyOpenSSL==23.0.0", "pyOpenSSL==23.0.0",
"orjson==3.8.6", "orjson==3.8.7",
"pip>=21.0,<23.1", "pip>=21.0,<23.1",
"python-slugify==4.0.1", "python-slugify==4.0.1",
"pyyaml==6.0", "pyyaml==6.0",
"requests==2.28.1", "requests==2.28.2",
"typing-extensions>=4.5.0,<5.0", "typing-extensions>=4.5.0,<5.0",
"voluptuous==0.13.1", "voluptuous==0.13.1",
"voluptuous-serialize==2.6.0", "voluptuous-serialize==2.6.0",

View File

@ -18,11 +18,11 @@ lru-dict==1.1.8
PyJWT==2.5.0 PyJWT==2.5.0
cryptography==39.0.1 cryptography==39.0.1
pyOpenSSL==23.0.0 pyOpenSSL==23.0.0
orjson==3.8.6 orjson==3.8.7
pip>=21.0,<23.1 pip>=21.0,<23.1
python-slugify==4.0.1 python-slugify==4.0.1
pyyaml==6.0 pyyaml==6.0
requests==2.28.1 requests==2.28.2
typing-extensions>=4.5.0,<5.0 typing-extensions>=4.5.0,<5.0
voluptuous==0.13.1 voluptuous==0.13.1
voluptuous-serialize==2.6.0 voluptuous-serialize==2.6.0

View File

@ -907,7 +907,7 @@ hole==0.8.0
holidays==0.18.0 holidays==0.18.0
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20230301.0 home-assistant-frontend==20230302.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.2.28 home-assistant-intents==2023.2.28
@ -1225,7 +1225,7 @@ nsapi==3.0.5
nsw-fuel-api-client==1.1.0 nsw-fuel-api-client==1.1.0
# homeassistant.components.nuheat # homeassistant.components.nuheat
nuheat==1.0.0 nuheat==1.0.1
# homeassistant.components.numato # homeassistant.components.numato
numato-gpio==0.10.0 numato-gpio==0.10.0
@ -1430,7 +1430,7 @@ py-canary==0.5.3
py-cpuinfo==8.0.0 py-cpuinfo==8.0.0
# homeassistant.components.dormakaba_dkey # homeassistant.components.dormakaba_dkey
py-dormakaba-dkey==1.0.3 py-dormakaba-dkey==1.0.4
# homeassistant.components.melissa # homeassistant.components.melissa
py-melissa-climate==2.1.4 py-melissa-climate==2.1.4
@ -1473,7 +1473,7 @@ pyRFXtrx==0.30.1
pySwitchmate==0.5.1 pySwitchmate==0.5.1
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.26.13 pyTibber==0.27.0
# homeassistant.components.dlink # homeassistant.components.dlink
pyW215==0.7.0 pyW215==0.7.0

View File

@ -690,7 +690,7 @@ hole==0.8.0
holidays==0.18.0 holidays==0.18.0
# homeassistant.components.frontend # homeassistant.components.frontend
home-assistant-frontend==20230301.0 home-assistant-frontend==20230302.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.2.28 home-assistant-intents==2023.2.28
@ -903,7 +903,7 @@ notify-events==1.0.4
nsw-fuel-api-client==1.1.0 nsw-fuel-api-client==1.1.0
# homeassistant.components.nuheat # homeassistant.components.nuheat
nuheat==1.0.0 nuheat==1.0.1
# homeassistant.components.numato # homeassistant.components.numato
numato-gpio==0.10.0 numato-gpio==0.10.0
@ -1045,7 +1045,7 @@ py-canary==0.5.3
py-cpuinfo==8.0.0 py-cpuinfo==8.0.0
# homeassistant.components.dormakaba_dkey # homeassistant.components.dormakaba_dkey
py-dormakaba-dkey==1.0.3 py-dormakaba-dkey==1.0.4
# homeassistant.components.melissa # homeassistant.components.melissa
py-melissa-climate==2.1.4 py-melissa-climate==2.1.4
@ -1076,7 +1076,7 @@ pyMetno==0.9.0
pyRFXtrx==0.30.1 pyRFXtrx==0.30.1
# homeassistant.components.tibber # homeassistant.components.tibber
pyTibber==0.26.13 pyTibber==0.27.0
# homeassistant.components.dlink # homeassistant.components.dlink
pyW215==0.7.0 pyW215==0.7.0

View File

@ -77,16 +77,17 @@ def patch_file_upload(return_value=FIXTURE_KEYRING, side_effect=None):
side_effect=side_effect, side_effect=side_effect,
), patch( ), patch(
"pathlib.Path.mkdir" "pathlib.Path.mkdir"
) as mkdir_mock: ) as mkdir_mock, patch(
file_path_mock = Mock() "shutil.move"
file_upload_mock.return_value.__enter__.return_value = file_path_mock ) as shutil_move_mock:
file_upload_mock.return_value.__enter__.return_value = Mock()
yield return_value yield return_value
if side_effect: if side_effect:
mkdir_mock.assert_not_called() mkdir_mock.assert_not_called()
file_path_mock.rename.assert_not_called() shutil_move_mock.assert_not_called()
else: else:
mkdir_mock.assert_called_once() mkdir_mock.assert_called_once()
file_path_mock.rename.assert_called_once() shutil_move_mock.assert_called_once()
def _gateway_descriptor( def _gateway_descriptor(

View File

@ -205,6 +205,47 @@ async def test_datetime_conversion(
assert state.state == test_timestamp.isoformat() 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( @pytest.mark.parametrize(
("device_class", "state_value", "provides"), ("device_class", "state_value", "provides"),
[ [