This commit is contained in:
Paulus Schoutsen 2023-02-02 20:52:38 -05:00 committed by GitHub
commit dbd8ffc282
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 378 additions and 131 deletions

View File

@ -2,7 +2,7 @@
"domain": "frontend", "domain": "frontend",
"name": "Home Assistant Frontend", "name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20230201.0"], "requirements": ["home-assistant-frontend==20230202.0"],
"dependencies": [ "dependencies": [
"api", "api",
"auth", "auth",

View File

@ -2,12 +2,12 @@
import asyncio import asyncio
from dataclasses import dataclass from dataclasses import dataclass
import AIOSomecomfort import aiosomecomfort
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant, callback 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 homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import ( from .const import (
@ -50,22 +50,19 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
username = config_entry.data[CONF_USERNAME] username = config_entry.data[CONF_USERNAME]
password = config_entry.data[CONF_PASSWORD] password = config_entry.data[CONF_PASSWORD]
client = AIOSomecomfort.AIOSomeComfort( client = aiosomecomfort.AIOSomeComfort(
username, password, session=async_get_clientsession(hass) username, password, session=async_get_clientsession(hass)
) )
try: try:
await client.login() await client.login()
await client.discover() await client.discover()
except AIOSomecomfort.AuthError as ex: except aiosomecomfort.device.AuthError as ex:
raise ConfigEntryNotReady( raise ConfigEntryAuthFailed("Incorrect Password") from ex
"Failed to initialize the Honeywell client: "
"Check your configuration (username, password), "
) from ex
except ( except (
AIOSomecomfort.ConnectionError, aiosomecomfort.device.ConnectionError,
AIOSomecomfort.ConnectionTimeout, aiosomecomfort.device.ConnectionTimeout,
asyncio.TimeoutError, asyncio.TimeoutError,
) as ex: ) as ex:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
@ -117,5 +114,5 @@ class HoneywellData:
"""Shared data for Honeywell.""" """Shared data for Honeywell."""
entry_id: str entry_id: str
client: AIOSomecomfort.AIOSomeComfort client: aiosomecomfort.AIOSomeComfort
devices: dict[str, AIOSomecomfort.device.Device] devices: dict[str, aiosomecomfort.device.Device]

View File

@ -4,7 +4,7 @@ from __future__ import annotations
import datetime import datetime
from typing import Any from typing import Any
import AIOSomecomfort import aiosomecomfort
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_HIGH,
@ -100,7 +100,7 @@ class HoneywellUSThermostat(ClimateEntity):
def __init__( def __init__(
self, self,
data: HoneywellData, data: HoneywellData,
device: AIOSomecomfort.device.Device, device: aiosomecomfort.device.Device,
cool_away_temp: int | None, cool_away_temp: int | None,
heat_away_temp: int | None, heat_away_temp: int | None,
) -> None: ) -> None:
@ -295,7 +295,7 @@ class HoneywellUSThermostat(ClimateEntity):
if mode == "heat": if mode == "heat":
await self._device.set_setpoint_heat(temperature) 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) _LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
@ -308,7 +308,7 @@ class HoneywellUSThermostat(ClimateEntity):
if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW): if temperature := kwargs.get(ATTR_TARGET_TEMP_LOW):
await self._device.set_setpoint_heat(temperature) 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) _LOGGER.error("Invalid temperature %.1f: %s", temperature, err)
async def async_set_fan_mode(self, fan_mode: str) -> None: async def async_set_fan_mode(self, fan_mode: str) -> None:
@ -330,7 +330,7 @@ class HoneywellUSThermostat(ClimateEntity):
try: try:
# Get current mode # Get current mode
mode = self._device.system_mode mode = self._device.system_mode
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode") _LOGGER.error("Can not get system mode")
return return
try: try:
@ -344,8 +344,7 @@ class HoneywellUSThermostat(ClimateEntity):
await self._device.set_hold_heat(True) await self._device.set_hold_heat(True)
await self._device.set_setpoint_heat(self._heat_away_temp) await self._device.set_setpoint_heat(self._heat_away_temp)
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
_LOGGER.error( _LOGGER.error(
"Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f",
mode, mode,
@ -358,7 +357,7 @@ class HoneywellUSThermostat(ClimateEntity):
try: try:
# Get current mode # Get current mode
mode = self._device.system_mode mode = self._device.system_mode
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not get system mode") _LOGGER.error("Can not get system mode")
return return
# Check that we got a valid mode back # Check that we got a valid mode back
@ -370,7 +369,7 @@ class HoneywellUSThermostat(ClimateEntity):
if mode in HEATING_MODES: if mode in HEATING_MODES:
await self._device.set_hold_heat(True) await self._device.set_hold_heat(True)
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
_LOGGER.error("Couldn't set permanent hold") _LOGGER.error("Couldn't set permanent hold")
else: else:
_LOGGER.error("Invalid system mode returned: %s", mode) _LOGGER.error("Invalid system mode returned: %s", mode)
@ -382,7 +381,7 @@ class HoneywellUSThermostat(ClimateEntity):
# Disabling all hold modes # Disabling all hold modes
await self._device.set_hold_cool(False) await self._device.set_hold_cool(False)
await self._device.set_hold_heat(False) await self._device.set_hold_heat(False)
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
_LOGGER.error("Can not stop hold mode") _LOGGER.error("Can not stop hold mode")
async def async_set_preset_mode(self, preset_mode: str) -> None: async def async_set_preset_mode(self, preset_mode: str) -> None:
@ -411,13 +410,13 @@ class HoneywellUSThermostat(ClimateEntity):
try: try:
await self._device.refresh() await self._device.refresh()
except ( except (
AIOSomecomfort.SomeComfortError, aiosomecomfort.SomeComfortError,
OSError, OSError,
): ):
try: try:
await self._data.client.login() await self._data.client.login()
except AIOSomecomfort.SomeComfortError: except aiosomecomfort.SomeComfortError:
self._attr_available = False self._attr_available = False
await self.hass.async_create_task( await self.hass.async_create_task(
self.hass.config_entries.async_reload(self._data.entry_id) self.hass.config_entries.async_reload(self._data.entry_id)

View File

@ -2,8 +2,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from collections.abc import Mapping
from typing import Any
import AIOSomecomfort import aiosomecomfort
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@ -20,11 +22,67 @@ from .const import (
DOMAIN, DOMAIN,
) )
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a honeywell config flow.""" """Handle a honeywell config flow."""
VERSION = 1 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: async def async_step_user(self, user_input=None) -> FlowResult:
"""Create config entry. Show the setup form to the user.""" """Create config entry. Show the setup form to the user."""
@ -32,11 +90,11 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if user_input is not None: if user_input is not None:
try: try:
await self.is_valid(**user_input) await self.is_valid(**user_input)
except AIOSomecomfort.AuthError: except aiosomecomfort.AuthError:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
except ( except (
AIOSomecomfort.ConnectionError, aiosomecomfort.ConnectionError,
AIOSomecomfort.ConnectionTimeout, aiosomecomfort.ConnectionTimeout,
asyncio.TimeoutError, asyncio.TimeoutError,
): ):
errors["base"] = "cannot_connect" errors["base"] = "cannot_connect"
@ -57,7 +115,7 @@ class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def is_valid(self, **kwargs) -> bool: async def is_valid(self, **kwargs) -> bool:
"""Check if login credentials are valid.""" """Check if login credentials are valid."""
client = AIOSomecomfort.AIOSomeComfort( client = aiosomecomfort.AIOSomeComfort(
kwargs[CONF_USERNAME], kwargs[CONF_USERNAME],
kwargs[CONF_PASSWORD], kwargs[CONF_PASSWORD],
session=async_get_clientsession(self.hass), session=async_get_clientsession(self.hass),

View File

@ -3,7 +3,7 @@
"name": "Honeywell Total Connect Comfort (US)", "name": "Honeywell Total Connect Comfort (US)",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/honeywell", "documentation": "https://www.home-assistant.io/integrations/honeywell",
"requirements": ["aiosomecomfort==0.0.3"], "requirements": ["aiosomecomfort==0.0.6"],
"codeowners": ["@rdfurman", "@mkmer"], "codeowners": ["@rdfurman", "@mkmer"],
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["somecomfort"] "loggers": ["somecomfort"]

View File

@ -5,7 +5,7 @@ from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any from typing import Any
from AIOSomecomfort.device import Device from aiosomecomfort.device import Device
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorDeviceClass, SensorDeviceClass,

View File

@ -255,7 +255,7 @@ NODE_FILTERS: dict[Platform, dict[str, list[str]]] = {
FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"], FILTER_STATES: ["open", "closed", "closing", "opening", "stopped"],
FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"], FILTER_NODE_DEF_ID: ["DimmerMotorSwitch_ADV"],
FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER], FILTER_INSTEON_TYPE: [TYPE_CATEGORY_COVER],
FILTER_ZWAVE_CAT: [], FILTER_ZWAVE_CAT: ["106", "107"],
}, },
Platform.LIGHT: { Platform.LIGHT: {
FILTER_UOM: ["51"], FILTER_UOM: ["51"],

View File

@ -36,8 +36,12 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
BaseUnitConverter, BaseUnitConverter,
DataRateConverter,
DistanceConverter, DistanceConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter, EnergyConverter,
InformationConverter,
MassConverter, MassConverter,
PowerConverter, PowerConverter,
PressureConverter, PressureConverter,
@ -128,8 +132,15 @@ QUERY_STATISTIC_META = [
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { 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: 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: EnergyConverter for unit in EnergyConverter.VALID_UNITS},
**{unit: InformationConverter for unit in InformationConverter.VALID_UNITS},
**{unit: MassConverter for unit in MassConverter.VALID_UNITS}, **{unit: MassConverter for unit in MassConverter.VALID_UNITS},
**{unit: PowerConverter for unit in PowerConverter.VALID_UNITS}, **{unit: PowerConverter for unit in PowerConverter.VALID_UNITS},
**{unit: PressureConverter for unit in PressureConverter.VALID_UNITS}, **{unit: PressureConverter for unit in PressureConverter.VALID_UNITS},

View File

@ -15,13 +15,18 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import JSON_DUMP from homeassistant.helpers.json import JSON_DUMP
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
DataRateConverter,
DistanceConverter, DistanceConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter, EnergyConverter,
InformationConverter,
MassConverter, MassConverter,
PowerConverter, PowerConverter,
PressureConverter, PressureConverter,
SpeedConverter, SpeedConverter,
TemperatureConverter, TemperatureConverter,
UnitlessRatioConverter,
VolumeConverter, VolumeConverter,
) )
@ -47,6 +52,24 @@ from .util import (
_LOGGER: logging.Logger = logging.getLogger(__package__) _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 @callback
def async_setup(hass: HomeAssistant) -> None: def async_setup(hass: HomeAssistant) -> None:
@ -93,18 +116,7 @@ def _ws_get_statistic_during_period(
vol.Optional("types"): vol.All( vol.Optional("types"): vol.All(
[vol.Any("max", "mean", "min", "change")], vol.Coerce(set) [vol.Any("max", "mean", "min", "change")], vol.Coerce(set)
), ),
vol.Optional("units"): vol.Schema( vol.Optional("units"): UNIT_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),
}
),
**PERIOD_SCHEMA.schema, **PERIOD_SCHEMA.schema,
} }
) )
@ -211,18 +223,7 @@ async def ws_handle_get_statistics_during_period(
vol.Optional("end_time"): str, vol.Optional("end_time"): str,
vol.Optional("statistic_ids"): [str], vol.Optional("statistic_ids"): [str],
vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"), vol.Required("period"): vol.Any("5minute", "hour", "day", "week", "month"),
vol.Optional("units"): vol.Schema( vol.Optional("units"): UNIT_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("types"): vol.All( vol.Optional("types"): vol.All(
[vol.Any("last_reset", "max", "mean", "min", "state", "sum")], [vol.Any("last_reset", "max", "mean", "min", "state", "sum")],
vol.Coerce(set), vol.Coerce(set),

View File

@ -257,7 +257,7 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
device_class=SensorDeviceClass.ENERGY, device_class=SensorDeviceClass.ENERGY,
name="Battery available energy", name="Battery available energy",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.TOTAL,
), ),
RenaultSensorEntityDescription( RenaultSensorEntityDescription(
key="battery_temperature", key="battery_temperature",

View File

@ -9,14 +9,7 @@ import logging
from aiohttp import ClientConnectorError from aiohttp import ClientConnectorError
import async_timeout import async_timeout
from reolink_aio.exceptions import ( from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
ApiError,
InvalidContentTypeError,
LoginError,
NoDataError,
ReolinkError,
UnexpectedDataError,
)
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.const import EVENT_HOMEASSISTANT_STOP, Platform
@ -48,17 +41,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
try: try:
await host.async_init() await host.async_init()
except UserNotAdmin as err: except (UserNotAdmin, CredentialsInvalidError) as err:
await host.stop()
raise ConfigEntryAuthFailed(err) from err raise ConfigEntryAuthFailed(err) from err
except ( except (
ClientConnectorError, ClientConnectorError,
asyncio.TimeoutError, asyncio.TimeoutError,
ApiError,
InvalidContentTypeError,
LoginError,
NoDataError,
ReolinkException, ReolinkException,
UnexpectedDataError, ReolinkError,
) as err: ) as err:
await host.stop() await host.stop()
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
@ -90,7 +80,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL), update_interval=timedelta(seconds=DEVICE_UPDATE_INTERVAL),
) )
# Fetch initial data so we have data when entities subscribe # 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( hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = ReolinkData(
host=host, host=host,

View File

@ -9,7 +9,7 @@ from typing import Any
import aiohttp import aiohttp
from aiohttp.web import Request from aiohttp.web import Request
from reolink_aio.api import Host 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.components import webhook
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
@ -76,7 +76,6 @@ class ReolinkHost:
raise ReolinkSetupException("Could not get mac address") raise ReolinkSetupException("Could not get mac address")
if not self._api.is_admin: if not self._api.is_admin:
await self.stop()
raise UserNotAdmin( raise UserNotAdmin(
f"User '{self._api.username}' has authorization level " f"User '{self._api.username}' has authorization level "
f"'{self._api.user_level}', only admin users can change camera settings" f"'{self._api.user_level}', only admin users can change camera settings"
@ -182,22 +181,19 @@ class ReolinkHost:
) )
return return
if await self._api.subscribe(self._webhook_url): await self._api.subscribe(self._webhook_url)
_LOGGER.debug(
"Host %s: subscribed successfully to webhook %s", _LOGGER.debug(
self._api.host, "Host %s: subscribed successfully to webhook %s",
self._webhook_url, self._api.host,
) self._webhook_url,
else: )
raise ReolinkWebhookException(
f"Host {self._api.host}: webhook subscription failed"
)
async def renew(self) -> None: async def renew(self) -> None:
"""Renew the subscription of motion events (lease time is 15 minutes).""" """Renew the subscription of motion events (lease time is 15 minutes)."""
try: try:
await self._renew() await self._renew()
except ReolinkWebhookException as err: except SubscriptionError as err:
if not self._lost_subscription: if not self._lost_subscription:
self._lost_subscription = True self._lost_subscription = True
_LOGGER.error( _LOGGER.error(
@ -220,25 +216,33 @@ class ReolinkHost:
return return
timer = self._api.renewtimer 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: if timer > SUBSCRIPTION_RENEW_THRESHOLD:
return return
if timer > 0: 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( _LOGGER.debug(
"Host %s successfully renewed Reolink subscription", self._api.host "Host %s successfully renewed Reolink subscription", self._api.host
) )
return 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): await self._api.subscribe(self._webhook_url)
raise ReolinkWebhookException(
f"Host {self._api.host}: webhook re-subscription failed"
)
_LOGGER.debug( _LOGGER.debug(
"Host %s: Reolink re-subscription successful after it was expired", "Host %s: Reolink re-subscription successful after it was expired",
self._api.host, self._api.host,
@ -246,7 +250,7 @@ class ReolinkHost:
async def register_webhook(self) -> None: async def register_webhook(self) -> None:
"""Register the webhook for motion events.""" """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 event_id = self.webhook_id
webhook.async_register( webhook.async_register(

View File

@ -3,7 +3,7 @@
"name": "Reolink IP NVR/camera", "name": "Reolink IP NVR/camera",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/reolink", "documentation": "https://www.home-assistant.io/integrations/reolink",
"requirements": ["reolink-aio==0.3.0"], "requirements": ["reolink-aio==0.3.2"],
"dependencies": ["webhook"], "dependencies": ["webhook"],
"codeowners": ["@starkillerOG"], "codeowners": ["@starkillerOG"],
"iot_class": "local_polling", "iot_class": "local_polling",

View File

@ -2,7 +2,7 @@
"domain": "synology_dsm", "domain": "synology_dsm",
"name": "Synology DSM", "name": "Synology DSM",
"documentation": "https://www.home-assistant.io/integrations/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"], "codeowners": ["@hacf-fr", "@Quentame", "@mib1185"],
"config_flow": true, "config_flow": true,
"ssdp": [ "ssdp": [

View File

@ -1,7 +1,7 @@
"""Representation of a sirenBinary.""" """Representation of a sirenBinary."""
from typing import Any 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.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -41,6 +41,13 @@ async def async_setup_entry(
class ZWaveMeSiren(ZWaveMeEntity, SirenEntity): class ZWaveMeSiren(ZWaveMeEntity, SirenEntity):
"""Representation of a ZWaveMe siren.""" """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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return the state of the siren.""" """Return the state of the siren."""

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 = 2 MINOR_VERSION: Final = 2
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

@ -757,7 +757,7 @@ class _ScriptRun:
with trace_path(condition_path): with trace_path(condition_path):
for idx, cond in enumerate(conditions): for idx, cond in enumerate(conditions):
with trace_path(str(idx)): with trace_path(str(idx)):
if not cond(hass, variables): if cond(hass, variables) is False:
return False return False
except exceptions.ConditionError as ex: except exceptions.ConditionError as ex:
_LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex) _LOGGER.warning("Error in '%s[%s]' evaluation: %s", name, idx, ex)

View File

@ -23,7 +23,7 @@ fnvhash==0.1.0
hass-nabucasa==0.61.0 hass-nabucasa==0.61.0
hassil==0.2.6 hassil==0.2.6
home-assistant-bluetooth==1.9.2 home-assistant-bluetooth==1.9.2
home-assistant-frontend==20230201.0 home-assistant-frontend==20230202.0
home-assistant-intents==2023.1.31 home-assistant-intents==2023.1.31
httpx==0.23.3 httpx==0.23.3
ifaddr==0.1.7 ifaddr==0.1.7

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "homeassistant" name = "homeassistant"
version = "2023.2.0" version = "2023.2.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"

View File

@ -276,7 +276,7 @@ aioskybell==22.7.0
aioslimproto==2.1.1 aioslimproto==2.1.1
# homeassistant.components.honeywell # homeassistant.components.honeywell
aiosomecomfort==0.0.3 aiosomecomfort==0.0.6
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -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==20230201.0 home-assistant-frontend==20230202.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.1.31 home-assistant-intents==2023.1.31
@ -1442,7 +1442,7 @@ py-schluter==0.1.7
py-sucks==0.9.8 py-sucks==0.9.8
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.0.2 py-synologydsm-api==2.1.1
# homeassistant.components.zabbix # homeassistant.components.zabbix
py-zabbix==1.1.7 py-zabbix==1.1.7
@ -2227,7 +2227,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.3.0 reolink-aio==0.3.2
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==6.0 restrictedpython==6.0

View File

@ -254,7 +254,7 @@ aioskybell==22.7.0
aioslimproto==2.1.1 aioslimproto==2.1.1
# homeassistant.components.honeywell # homeassistant.components.honeywell
aiosomecomfort==0.0.3 aiosomecomfort==0.0.6
# homeassistant.components.steamist # homeassistant.components.steamist
aiosteamist==0.3.2 aiosteamist==0.3.2
@ -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==20230201.0 home-assistant-frontend==20230202.0
# homeassistant.components.conversation # homeassistant.components.conversation
home-assistant-intents==2023.1.31 home-assistant-intents==2023.1.31
@ -1051,7 +1051,7 @@ py-melissa-climate==2.1.4
py-nightscout==1.2.2 py-nightscout==1.2.2
# homeassistant.components.synology_dsm # homeassistant.components.synology_dsm
py-synologydsm-api==2.0.2 py-synologydsm-api==2.1.1
# homeassistant.components.seventeentrack # homeassistant.components.seventeentrack
py17track==2021.12.2 py17track==2021.12.2
@ -1572,7 +1572,7 @@ regenmaschine==2022.11.0
renault-api==0.1.11 renault-api==0.1.11
# homeassistant.components.reolink # homeassistant.components.reolink
reolink-aio==0.3.0 reolink-aio==0.3.2
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==6.0 restrictedpython==6.0

View File

@ -2,7 +2,7 @@
from unittest.mock import AsyncMock, create_autospec, patch from unittest.mock import AsyncMock, create_autospec, patch
import AIOSomecomfort import aiosomecomfort
import pytest import pytest
from homeassistant.components.honeywell.const import DOMAIN from homeassistant.components.honeywell.const import DOMAIN
@ -30,7 +30,7 @@ def config_entry(config_data):
@pytest.fixture @pytest.fixture
def device(): def device():
"""Mock a somecomfort.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.deviceid = 1234567
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -48,7 +48,7 @@ def device():
@pytest.fixture @pytest.fixture
def device_with_outdoor_sensor(): def device_with_outdoor_sensor():
"""Mock a somecomfort.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.deviceid = 1234567
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -67,7 +67,7 @@ def device_with_outdoor_sensor():
@pytest.fixture @pytest.fixture
def another_device(): def another_device():
"""Mock a somecomfort.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.deviceid = 7654321
mock_device._data = { mock_device._data = {
"canControlHumidification": False, "canControlHumidification": False,
@ -85,7 +85,7 @@ def another_device():
@pytest.fixture @pytest.fixture
def location(device): def location(device):
"""Mock a somecomfort.Location.""" """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.locationid.return_value = "location1"
mock_location.devices_by_id = {device.deviceid: device} mock_location.devices_by_id = {device.deviceid: device}
return mock_location return mock_location
@ -94,13 +94,13 @@ def location(device):
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def client(location): def client(location):
"""Mock a somecomfort.SomeComfort client.""" """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.locations_by_id = {location.locationid: location}
client_mock.login = AsyncMock(return_value=True) client_mock.login = AsyncMock(return_value=True)
client_mock.discover = AsyncMock() client_mock.discover = AsyncMock()
with patch( with patch(
"homeassistant.components.honeywell.AIOSomecomfort.AIOSomeComfort" "homeassistant.components.honeywell.aiosomecomfort.AIOSomeComfort"
) as sc_class_mock: ) as sc_class_mock:
sc_class_mock.return_value = client_mock sc_class_mock.return_value = client_mock
yield client_mock yield client_mock

View File

@ -1,7 +1,9 @@
"""Tests for honeywell config flow.""" """Tests for honeywell config flow."""
import asyncio
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
import AIOSomecomfort import aiosomecomfort
import pytest
from homeassistant import data_entry_flow from homeassistant import data_entry_flow
from homeassistant.components.honeywell.const import ( from homeassistant.components.honeywell.const import (
@ -9,8 +11,10 @@ from homeassistant.components.honeywell.const import (
CONF_HEAT_AWAY_TEMPERATURE, CONF_HEAT_AWAY_TEMPERATURE,
DOMAIN, 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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType
from tests.common import MockConfigEntry 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: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None:
"""Test that an error message is shown on connection fail.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG 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: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None:
"""Test that an error message is shown on login fail.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG
@ -116,3 +119,137 @@ async def test_create_option_entry(
CONF_COOL_AWAY_TEMPERATURE: 1, CONF_COOL_AWAY_TEMPERATURE: 1,
CONF_HEAT_AWAY_TEMPERATURE: 2, 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"}

View File

@ -2,7 +2,7 @@
from unittest.mock import create_autospec, patch from unittest.mock import create_autospec, patch
import AIOSomecomfort import aiosomecomfort
from homeassistant.components.honeywell.const import ( from homeassistant.components.honeywell.const import (
CONF_COOL_AWAY_TEMPERATURE, 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 hass: HomeAssistant, caplog, config_entry: MockConfigEntry, device, client
) -> None: ) -> None:
"""Test Honeywell TCC API returning duplicate device IDs.""" """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.locationid.return_value = "location2"
mock_location2.devices_by_id = {device.deviceid: device} mock_location2.devices_by_id = {device.deviceid: device}
client.locations_by_id["location2"] = mock_location2 client.locations_by_id["location2"] = mock_location2

View File

@ -1,6 +1,6 @@
"""Test honeywell sensor.""" """Test honeywell sensor."""
from AIOSomecomfort.device import Device from aiosomecomfort.device import Device
from AIOSomecomfort.location import Location from aiosomecomfort.location import Location
import pytest import pytest
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant

View File

@ -139,7 +139,7 @@ MOCK_VEHICLES = {
ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY,
ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy",
ATTR_STATE: "31", ATTR_STATE: "31",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_STATE_CLASS: SensorStateClass.TOTAL,
ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy",
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
}, },
@ -368,7 +368,7 @@ MOCK_VEHICLES = {
ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY,
ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy",
ATTR_STATE: "0", ATTR_STATE: "0",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_STATE_CLASS: SensorStateClass.TOTAL,
ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy", ATTR_UNIQUE_ID: "vf1aaaaa555777999_battery_available_energy",
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
}, },
@ -597,7 +597,7 @@ MOCK_VEHICLES = {
ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY,
ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy", ATTR_ENTITY_ID: "sensor.reg_number_battery_available_energy",
ATTR_STATE: "31", ATTR_STATE: "31",
ATTR_STATE_CLASS: SensorStateClass.MEASUREMENT, ATTR_STATE_CLASS: SensorStateClass.TOTAL,
ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy", ATTR_UNIQUE_ID: "vf1aaaaa555777123_battery_available_energy",
ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR,
}, },

View File

@ -2915,6 +2915,45 @@ async def test_if(
assert_action_trace(expected_trace) 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( async def test_if_condition_validation(
hass: HomeAssistant, caplog: pytest.LogCaptureFixture hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None: ) -> None: