diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index b33668cde4cc41..ba4185fa83806b 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==20230201.0"], + "requirements": ["home-assistant-frontend==20230202.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 3316d2852e72e6..93c29446a53157 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -2,12 +2,12 @@ import asyncio from dataclasses import dataclass -import AIOSomecomfort +import aiosomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -50,22 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b username = config_entry.data[CONF_USERNAME] password = config_entry.data[CONF_PASSWORD] - client = AIOSomecomfort.AIOSomeComfort( + client = aiosomecomfort.AIOSomeComfort( username, password, session=async_get_clientsession(hass) ) try: await client.login() await client.discover() - except AIOSomecomfort.AuthError as ex: - raise ConfigEntryNotReady( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - ) from ex + except aiosomecomfort.device.AuthError as ex: + raise ConfigEntryAuthFailed("Incorrect Password") from ex except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + aiosomecomfort.device.ConnectionError, + aiosomecomfort.device.ConnectionTimeout, asyncio.TimeoutError, ) as ex: raise ConfigEntryNotReady( @@ -117,5 +114,5 @@ class HoneywellData: """Shared data for Honeywell.""" entry_id: str - client: AIOSomecomfort.AIOSomeComfort - devices: dict[str, AIOSomecomfort.device.Device] + client: aiosomecomfort.AIOSomeComfort + devices: dict[str, aiosomecomfort.device.Device] diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 0267eb32e4727c..1296dfbfdb3a9a 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -4,7 +4,7 @@ import datetime from typing import Any -import AIOSomecomfort +import aiosomecomfort from homeassistant.components.climate import ( ATTR_TARGET_TEMP_HIGH, @@ -100,7 +100,7 @@ class HoneywellUSThermostat(ClimateEntity): def __init__( self, data: HoneywellData, - device: AIOSomecomfort.device.Device, + device: aiosomecomfort.device.Device, cool_away_temp: int | None, heat_away_temp: int | None, ) -> None: @@ -295,7 +295,7 @@ async def _set_temperature(self, **kwargs) -> None: if mode == "heat": await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: + except aiosomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_temperature(self, **kwargs: Any) -> None: @@ -308,7 +308,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): await self._device.set_setpoint_heat(temperature) - except AIOSomecomfort.SomeComfortError as err: + except aiosomecomfort.SomeComfortError as err: _LOGGER.error("Invalid temperature %.1f: %s", temperature, err) async def async_set_fan_mode(self, fan_mode: str) -> None: @@ -330,7 +330,7 @@ async def _turn_away_mode_on(self) -> None: try: # Get current mode mode = self._device.system_mode - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return try: @@ -344,8 +344,7 @@ async def _turn_away_mode_on(self) -> None: await self._device.set_hold_heat(True) await self._device.set_setpoint_heat(self._heat_away_temp) - except AIOSomecomfort.SomeComfortError: - + except aiosomecomfort.SomeComfortError: _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", mode, @@ -358,7 +357,7 @@ async def _turn_hold_mode_on(self) -> None: try: # Get current mode mode = self._device.system_mode - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not get system mode") return # Check that we got a valid mode back @@ -370,7 +369,7 @@ async def _turn_hold_mode_on(self) -> None: if mode in HEATING_MODES: await self._device.set_hold_heat(True) - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Couldn't set permanent hold") else: _LOGGER.error("Invalid system mode returned: %s", mode) @@ -382,7 +381,7 @@ async def _turn_away_mode_off(self) -> None: # Disabling all hold modes await self._device.set_hold_cool(False) await self._device.set_hold_heat(False) - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: _LOGGER.error("Can not stop hold mode") async def async_set_preset_mode(self, preset_mode: str) -> None: @@ -411,13 +410,13 @@ async def async_update(self) -> None: try: await self._device.refresh() except ( - AIOSomecomfort.SomeComfortError, + aiosomecomfort.SomeComfortError, OSError, ): try: await self._data.client.login() - except AIOSomecomfort.SomeComfortError: + except aiosomecomfort.SomeComfortError: self._attr_available = False await self.hass.async_create_task( self.hass.config_entries.async_reload(self._data.entry_id) diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 9f630d90fbeb72..76d208f72f3092 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -2,8 +2,10 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping +from typing import Any -import AIOSomecomfort +import aiosomecomfort import voluptuous as vol from homeassistant import config_entries @@ -20,11 +22,67 @@ DOMAIN, ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a honeywell config flow.""" VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with Honeywell.""" + + 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: + """Confirm re-authentication with Honeywell.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await self.is_valid( + username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + ) + + except aiosomecomfort.AuthError: + errors["base"] = "invalid_auth" + + except ( + aiosomecomfort.ConnectionError, + aiosomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ): + errors["base"] = "cannot_connect" + + else: + + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await 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=REAUTH_SCHEMA, + errors=errors, + ) async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" @@ -32,11 +90,11 @@ async def async_step_user(self, user_input=None) -> FlowResult: if user_input is not None: try: await self.is_valid(**user_input) - except AIOSomecomfort.AuthError: + except aiosomecomfort.AuthError: errors["base"] = "invalid_auth" except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + aiosomecomfort.ConnectionError, + aiosomecomfort.ConnectionTimeout, asyncio.TimeoutError, ): errors["base"] = "cannot_connect" @@ -57,7 +115,7 @@ async def async_step_user(self, user_input=None) -> FlowResult: async def is_valid(self, **kwargs) -> bool: """Check if login credentials are valid.""" - client = AIOSomecomfort.AIOSomeComfort( + client = aiosomecomfort.AIOSomeComfort( kwargs[CONF_USERNAME], kwargs[CONF_PASSWORD], session=async_get_clientsession(self.hass), diff --git a/homeassistant/components/honeywell/manifest.json b/homeassistant/components/honeywell/manifest.json index 7eb07711b093f2..974123825fa256 100644 --- a/homeassistant/components/honeywell/manifest.json +++ b/homeassistant/components/honeywell/manifest.json @@ -3,7 +3,7 @@ "name": "Honeywell Total Connect Comfort (US)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/honeywell", - "requirements": ["aiosomecomfort==0.0.3"], + "requirements": ["aiosomecomfort==0.0.6"], "codeowners": ["@rdfurman", "@mkmer"], "iot_class": "cloud_polling", "loggers": ["somecomfort"] diff --git a/homeassistant/components/honeywell/sensor.py b/homeassistant/components/honeywell/sensor.py index 59f00472700aa8..9e85ba7727aa10 100644 --- a/homeassistant/components/honeywell/sensor.py +++ b/homeassistant/components/honeywell/sensor.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from typing import Any -from AIOSomecomfort.device import Device +from aiosomecomfort.device import Device from homeassistant.components.sensor import ( SensorDeviceClass, diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py index 211939f8eb618d..95e68c5fa1156d 100644 --- a/homeassistant/components/isy994/const.py +++ b/homeassistant/components/isy994/const.py @@ -255,7 +255,7 @@ FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"], FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER], - FILTER_ZWAVE_CAT: [], + FILTER_ZWAVE_CAT: ["106", "107"], }, Platform.LIGHT: { FILTER_UOM: ["51"], diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 4a39abd9f6365b..7a5fba6ea60117 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -36,8 +36,12 @@ from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( BaseUnitConverter, + DataRateConverter, DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, @@ -128,8 +132,15 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { + **{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS}, **{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS}, + **{unit: ElectricCurrentConverter for unit in ElectricCurrentConverter.VALID_UNITS}, + **{ + unit: ElectricPotentialConverter + for unit in ElectricPotentialConverter.VALID_UNITS + }, **{unit: EnergyConverter for unit in EnergyConverter.VALID_UNITS}, + **{unit: InformationConverter for unit in InformationConverter.VALID_UNITS}, **{unit: MassConverter for unit in MassConverter.VALID_UNITS}, **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index c63e5bc43ca06a..8733d675069f05 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -15,13 +15,18 @@ from homeassistant.helpers.json import JSON_DUMP from homeassistant.util import dt as dt_util from homeassistant.util.unit_conversion import ( + DataRateConverter, DistanceConverter, + ElectricCurrentConverter, + ElectricPotentialConverter, EnergyConverter, + InformationConverter, MassConverter, PowerConverter, PressureConverter, SpeedConverter, TemperatureConverter, + UnitlessRatioConverter, VolumeConverter, ) @@ -47,6 +52,24 @@ _LOGGER: logging.Logger = logging.getLogger(__package__) +UNIT_SCHEMA = vol.Schema( + { + vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS), + vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), + vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS), + vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS), + vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), + vol.Optional("information"): vol.In(InformationConverter.VALID_UNITS), + vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), + vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), + vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), + vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), + vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), + vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), + vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + } +) + @callback def async_setup(hass: HomeAssistant) -> None: @@ -93,18 +116,7 @@ def _ws_get_statistic_during_period( vol.Optional("types"): vol.All( [vol.Any("max", "mean", "min", "change")], vol.Coerce(set) ), - vol.Optional("units"): vol.Schema( - { - vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), - vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), - vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), - vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), - vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), - vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), - vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), - } - ), + vol.Optional("units"): UNIT_SCHEMA, **PERIOD_SCHEMA.schema, } ) @@ -211,18 +223,7 @@ 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", "week", "month"), - vol.Optional("units"): vol.Schema( - { - vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS), - vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS), - vol.Optional("mass"): vol.In(MassConverter.VALID_UNITS), - vol.Optional("power"): vol.In(PowerConverter.VALID_UNITS), - vol.Optional("pressure"): vol.In(PressureConverter.VALID_UNITS), - vol.Optional("speed"): vol.In(SpeedConverter.VALID_UNITS), - vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), - vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), - } - ), + vol.Optional("units"): UNIT_SCHEMA, vol.Optional("types"): vol.All( [vol.Any("last_reset", "max", "mean", "min", "state", "sum")], vol.Coerce(set), diff --git a/homeassistant/components/renault/sensor.py b/homeassistant/components/renault/sensor.py index 679fbbd370f5f0..52538d1e873951 100644 --- a/homeassistant/components/renault/sensor.py +++ b/homeassistant/components/renault/sensor.py @@ -257,7 +257,7 @@ def _get_utc_value(entity: RenaultSensor[T]) -> datetime: device_class=SensorDeviceClass.ENERGY, name="Battery available energy", native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - state_class=SensorStateClass.MEASUREMENT, + state_class=SensorStateClass.TOTAL, ), RenaultSensorEntityDescription( key="battery_temperature", diff --git a/homeassistant/components/reolink/__init__.py b/homeassistant/components/reolink/__init__.py index c20aff637ec183..f15ce90427c696 100644 --- a/homeassistant/components/reolink/__init__.py +++ b/homeassistant/components/reolink/__init__.py @@ -9,14 +9,7 @@ from aiohttp import ClientConnectorError import async_timeout -from reolink_aio.exceptions import ( - ApiError, - InvalidContentTypeError, - LoginError, - NoDataError, - ReolinkError, - UnexpectedDataError, -) +from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform @@ -48,17 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b try: await host.async_init() - except UserNotAdmin as err: + except (UserNotAdmin, CredentialsInvalidError) as err: + await host.stop() raise ConfigEntryAuthFailed(err) from err except ( ClientConnectorError, asyncio.TimeoutError, - ApiError, - InvalidContentTypeError, - LoginError, - NoDataError, ReolinkException, - UnexpectedDataError, + ReolinkError, ) as err: await host.stop() raise ConfigEntryNotReady( @@ -90,7 +80,11 @@ async def async_device_config_update(): update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL), ) # Fetch initial data so we have data when entities subscribe - await coordinator_device_config_update.async_config_entry_first_refresh() + try: + await coordinator_device_config_update.async_config_entry_first_refresh() + except ConfigEntryNotReady as err: + await host.stop() + raise err hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData( host=host, diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index 3e0731ac8ced5e..e44623e1a1e976 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -9,7 +9,7 @@ import aiohttp from aiohttp.web import Request from reolink_aio.api import Host -from reolink_aio.exceptions import ReolinkError +from reolink_aio.exceptions import ReolinkError, SubscriptionError from homeassistant.components import webhook from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME @@ -76,7 +76,6 @@ async def async_init(self) -> None: raise ReolinkSetupException("Could not get mac address") if not self._api.is_admin: - await self.stop() raise UserNotAdmin( f"User '{self._api.username}' has authorization level " f"'{self._api.user_level}', only admin users can change camera settings" @@ -182,22 +181,19 @@ async def subscribe(self) -> None: ) return - if await self._api.subscribe(self._webhook_url): - _LOGGER.debug( - "Host %s: subscribed successfully to webhook %s", - self._api.host, - self._webhook_url, - ) - else: - raise ReolinkWebhookException( - f"Host {self._api.host}: webhook subscription failed" - ) + await self._api.subscribe(self._webhook_url) + + _LOGGER.debug( + "Host %s: subscribed successfully to webhook %s", + self._api.host, + self._webhook_url, + ) async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" try: await self._renew() - except ReolinkWebhookException as err: + except SubscriptionError as err: if not self._lost_subscription: self._lost_subscription = True _LOGGER.error( @@ -220,25 +216,33 @@ async def _renew(self) -> None: return timer = self._api.renewtimer + _LOGGER.debug( + "Host %s:%s should renew subscription in: %i seconds", + self._api.host, + self._api.port, + timer, + ) if timer > SUBSCRIPTION_RENEW_THRESHOLD: return if timer > 0: - if await self._api.renew(): + try: + await self._api.renew() + except SubscriptionError as err: + _LOGGER.debug( + "Host %s: error renewing Reolink subscription, " + "trying to subscribe again: %s", + self._api.host, + err, + ) + else: _LOGGER.debug( "Host %s successfully renewed Reolink subscription", self._api.host ) return - _LOGGER.debug( - "Host %s: error renewing Reolink subscription, " - "trying to subscribe again", - self._api.host, - ) - if not await self._api.subscribe(self._webhook_url): - raise ReolinkWebhookException( - f"Host {self._api.host}: webhook re-subscription failed" - ) + await self._api.subscribe(self._webhook_url) + _LOGGER.debug( "Host %s: Reolink re-subscription successful after it was expired", self._api.host, @@ -246,7 +250,7 @@ async def _renew(self) -> None: async def register_webhook(self) -> None: """Register the webhook for motion events.""" - self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}" + self.webhook_id = f"{DOMAIN}_{self.unique_id.replace(':', '')}_ONVIF" event_id = self.webhook_id webhook.async_register( diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 88e2e3b7730955..ab9443140541eb 100644 --- a/homeassistant/components/reolink/manifest.json +++ b/homeassistant/components/reolink/manifest.json @@ -3,7 +3,7 @@ "name": "Reolink IP NVR/camera", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/reolink", - "requirements": ["reolink-aio==0.3.0"], + "requirements": ["reolink-aio==0.3.2"], "dependencies": ["webhook"], "codeowners": ["@starkillerOG"], "iot_class": "local_polling", diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 6d0012156db61c..14f45fbf637f7c 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["py-synologydsm-api==2.0.2"], + "requirements": ["py-synologydsm-api==2.1.1"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/zwave_me/siren.py b/homeassistant/components/zwave_me/siren.py index f59face65d44ed..c6757f61ad7ec7 100644 --- a/homeassistant/components/zwave_me/siren.py +++ b/homeassistant/components/zwave_me/siren.py @@ -1,7 +1,7 @@ """Representation of a sirenBinary.""" from typing import Any -from homeassistant.components.siren import SirenEntity +from homeassistant.components.siren import SirenEntity, SirenEntityFeature from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -41,6 +41,13 @@ def add_new_device(new_device): class ZWaveMeSiren(ZWaveMeEntity, SirenEntity): """Representation of a ZWaveMe siren.""" + def __init__(self, controller, device): + """Initialize the device.""" + super().__init__(controller, device) + self._attr_supported_features = ( + SirenEntityFeature.TURN_ON | SirenEntityFeature.TURN_OFF + ) + @property def is_on(self) -> bool: """Return the state of the siren.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index ee5d60a32be677..070f9a37862295 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -8,7 +8,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2023 MINOR_VERSION: Final = 2 -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/helpers/script.py b/homeassistant/helpers/script.py index 34a0d4de2d4f13..02fa9dc7806ea0 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -757,7 +757,7 @@ def traced_test_conditions(hass, variables): with trace_path(condition_path): for idx, cond in enumerate(conditions): with trace_path(str(idx)): - if not cond(hass, variables): + if cond(hass, variables) is False: return False except exceptions.ConditionError as ex: _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9101375adb35e1..8dc60525be17d8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -23,7 +23,7 @@ fnvhash==0.1.0 hass-nabucasa==0.61.0 hassil==0.2.6 home-assistant-bluetooth==1.9.2 -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 home-assistant-intents==2023.1.31 httpx==0.23.3 ifaddr==0.1.7 diff --git a/pyproject.toml b/pyproject.toml index 5c014aabd4b3f0..affc4b6446cb02 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2023.2.0" +version = "2023.2.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index 6ccb4d457e5650..ac62e334ec8184 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -276,7 +276,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.3 +aiosomecomfort==0.0.6 # homeassistant.components.steamist aiosteamist==0.3.2 @@ -907,7 +907,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 @@ -1442,7 +1442,7 @@ py-schluter==0.1.7 py-sucks==0.9.8 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.2 +py-synologydsm-api==2.1.1 # homeassistant.components.zabbix py-zabbix==1.1.7 @@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.0 +reolink-aio==0.3.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b91f5d6d144d11..52727f66ee476d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -254,7 +254,7 @@ aioskybell==22.7.0 aioslimproto==2.1.1 # homeassistant.components.honeywell -aiosomecomfort==0.0.3 +aiosomecomfort==0.0.6 # homeassistant.components.steamist aiosteamist==0.3.2 @@ -690,7 +690,7 @@ hole==0.8.0 holidays==0.18.0 # homeassistant.components.frontend -home-assistant-frontend==20230201.0 +home-assistant-frontend==20230202.0 # homeassistant.components.conversation home-assistant-intents==2023.1.31 @@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4 py-nightscout==1.2.2 # homeassistant.components.synology_dsm -py-synologydsm-api==2.0.2 +py-synologydsm-api==2.1.1 # homeassistant.components.seventeentrack py17track==2021.12.2 @@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0 renault-api==0.1.11 # homeassistant.components.reolink -reolink-aio==0.3.0 +reolink-aio==0.3.2 # homeassistant.components.python_script restrictedpython==6.0 diff --git a/tests/components/honeywell/conftest.py b/tests/components/honeywell/conftest.py index bead64c71d1272..95e1758ec221a7 100644 --- a/tests/components/honeywell/conftest.py +++ b/tests/components/honeywell/conftest.py @@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, create_autospec, patch -import AIOSomecomfort +import aiosomecomfort import pytest from homeassistant.components.honeywell.const import DOMAIN @@ -30,7 +30,7 @@ def config_entry(config_data): @pytest.fixture def device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -48,7 +48,7 @@ def device(): @pytest.fixture def device_with_outdoor_sensor(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 1234567 mock_device._data = { "canControlHumidification": False, @@ -67,7 +67,7 @@ def device_with_outdoor_sensor(): @pytest.fixture def another_device(): """Mock a somecomfort.Device.""" - mock_device = create_autospec(AIOSomecomfort.device.Device, instance=True) + mock_device = create_autospec(aiosomecomfort.device.Device, instance=True) mock_device.deviceid = 7654321 mock_device._data = { "canControlHumidification": False, @@ -85,7 +85,7 @@ def another_device(): @pytest.fixture def location(device): """Mock a somecomfort.Location.""" - mock_location = create_autospec(AIOSomecomfort.location.Location, instance=True) + mock_location = create_autospec(aiosomecomfort.location.Location, instance=True) mock_location.locationid.return_value = "location1" mock_location.devices_by_id = {device.deviceid: device} return mock_location @@ -94,13 +94,13 @@ def location(device): @pytest.fixture(autouse=True) def client(location): """Mock a somecomfort.SomeComfort client.""" - client_mock = create_autospec(AIOSomecomfort.AIOSomeComfort, instance=True) + client_mock = create_autospec(aiosomecomfort.AIOSomeComfort, instance=True) client_mock.locations_by_id = {location.locationid: location} client_mock.login = AsyncMock(return_value=True) client_mock.discover = AsyncMock() with patch( - "homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort" + "homeassistant.components.honeywell.aiosomecomfort.AIOSomeComfort" ) as sc_class_mock: sc_class_mock.return_value = client_mock yield client_mock diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 46ab48572f8deb..ff970a0e8c575b 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for honeywell config flow.""" +import asyncio from unittest.mock import MagicMock, patch -import AIOSomecomfort +import aiosomecomfort +import pytest from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( @@ -9,8 +11,10 @@ CONF_HEAT_AWAY_TEMPERATURE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,8 +39,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on connection fail.""" - client.login.side_effect = AIOSomecomfort.ConnectionError - + client.login.side_effect = aiosomecomfort.device.ConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -45,7 +48,7 @@ async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - client.login.side_effect = AIOSomecomfort.AuthError + client.login.side_effect = aiosomecomfort.device.AuthError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG @@ -116,3 +119,137 @@ async def test_create_option_entry( CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, } + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> None: + """Test an authorization error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = aiosomecomfort.device.AuthError + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "error", + [ + aiosomecomfort.device.ConnectionError, + aiosomecomfort.device.ConnectionTimeout, + asyncio.TimeoutError, + ], +) +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, client: MagicMock, error +) -> None: + """Test a connection error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = error + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/honeywell/test_init.py b/tests/components/honeywell/test_init.py index 4ecd2a3172db99..855d503401e949 100644 --- a/tests/components/honeywell/test_init.py +++ b/tests/components/honeywell/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import create_autospec, patch -import AIOSomecomfort +import aiosomecomfort from homeassistant.components.honeywell.const import ( CONF_COOL_AWAY_TEMPERATURE, @@ -46,7 +46,7 @@ async def test_setup_multiple_thermostats_with_same_deviceid( hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client ) -> None: """Test Honeywell TCC API returning duplicate device IDs.""" - mock_location2 = create_autospec(AIOSomecomfort.Location, instance=True) + mock_location2 = create_autospec(aiosomecomfort.Location, instance=True) mock_location2.locationid.return_value = "location2" mock_location2.devices_by_id = {device.deviceid: device} client.locations_by_id["location2"] = mock_location2 diff --git a/tests/components/honeywell/test_sensor.py b/tests/components/honeywell/test_sensor.py index 7ed047262bf612..c5367764d3d616 100644 --- a/tests/components/honeywell/test_sensor.py +++ b/tests/components/honeywell/test_sensor.py @@ -1,6 +1,6 @@ """Test honeywell sensor.""" -from AIOSomecomfort.device import Device -from AIOSomecomfort.location import Location +from aiosomecomfort.device import Device +from aiosomecomfort.location import Location import pytest from homeassistant.core import HomeAssistant diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index 5db47c5d58989d..b15b00b825fb66 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -139,7 +139,7 @@ ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, @@ -368,7 +368,7 @@ ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "0", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, @@ -597,7 +597,7 @@ ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_STATE: "31", - ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, + ATTR_STATE_CLASS: SensorStateClass.TOTAL, ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, }, diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index b44e7b7c458ab7..86172c6c7fda89 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2915,6 +2915,45 @@ async def test_if( assert_action_trace(expected_trace) +async def test_if_disabled( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test if action with a disabled condition.""" + sequence = cv.SCRIPT_SCHEMA( + { + "if": { + "alias": "if condition", + "condition": "template", + "value_template": "{{ var == 1 }}", + "enabled": "false", + }, + "then": { + "alias": "if then", + "event": "test_event", + "event_data": {"if": "then"}, + }, + "else": { + "alias": "if else", + "event": "test_event", + "event_data": {"if": "else"}, + }, + } + ) + + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + + await script_obj.async_run(context=Context()) + await hass.async_block_till_done() + + expected_trace = { + "0": [{"result": {"choice": "then"}}], + "0/if": [{"result": {"result": True}}], + "0/if/condition/0": [{"result": {"result": None}}], + "0/then/0": [{"result": {"event": "test_event", "event_data": {"if": "then"}}}], + } + assert_action_trace(expected_trace) + + async def test_if_condition_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: