mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
2024.9.3 (#126566)
This commit is contained in:
commit
06ea3a3014
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.8.0"],
|
||||
"requirements": ["airgradient==0.9.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
@ -4,13 +4,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
|
||||
from aioaseko import Aseko, AsekoNotLoggedIn
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
@ -22,28 +21,17 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Aseko Pool Live from a config entry."""
|
||||
account = MobileAccount(
|
||||
async_get_clientsession(hass),
|
||||
username=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||
|
||||
try:
|
||||
units = await account.get_units()
|
||||
except InvalidAuthCredentials as err:
|
||||
await aseko.login()
|
||||
except AsekoNotLoggedIn as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except APIUnavailable as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []
|
||||
|
||||
for unit in units:
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, unit)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))
|
||||
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@ -51,7 +39,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
@ -8,7 +8,6 @@ from dataclasses import dataclass
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
@ -25,26 +24,14 @@ from .entity import AsekoEntity
|
||||
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes an Aseko binary sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], bool]
|
||||
value_fn: Callable[[Unit], bool | None]
|
||||
|
||||
|
||||
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="water_flow",
|
||||
translation_key="water_flow",
|
||||
value_fn=lambda unit: unit.water_flow,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_alarm",
|
||||
translation_key="alarm",
|
||||
value_fn=lambda unit: unit.has_alarm,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_error",
|
||||
translation_key="error",
|
||||
value_fn=lambda unit: unit.has_error,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
translation_key="water_flow_to_probes",
|
||||
value_fn=lambda unit: unit.water_flow_to_probes,
|
||||
),
|
||||
)
|
||||
|
||||
@ -55,33 +42,22 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live binary sensors."""
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
AsekoUnitBinarySensorEntity(unit, coordinator, description)
|
||||
for unit, coordinator in data
|
||||
for description in UNIT_BINARY_SENSORS
|
||||
AsekoBinarySensorEntity(unit, coordinator, description)
|
||||
for description in BINARY_SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
)
|
||||
|
||||
|
||||
class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of a unit water flow binary sensor entity."""
|
||||
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of an Aseko binary sensor entity."""
|
||||
|
||||
entity_description: AsekoBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
entity_description: AsekoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the unit binary sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self._unit)
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
|
@ -6,12 +6,11 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
|
||||
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def get_account_info(self, email: str, password: str) -> dict:
|
||||
"""Get account info from the mobile API and the web API."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
web_account = WebAccount(session, email, password)
|
||||
web_account_info = await web_account.login()
|
||||
|
||||
aseko = Aseko(email, password)
|
||||
user = await aseko.login()
|
||||
return {
|
||||
CONF_EMAIL: email,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_UNIQUE_ID: web_account_info.user_id,
|
||||
CONF_UNIQUE_ID: user.user_id,
|
||||
}
|
||||
|
||||
async def async_step_user(
|
||||
@ -58,9 +54,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except APIUnavailable:
|
||||
except AsekoAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuthCredentials:
|
||||
except AsekoInvalidCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
@ -122,9 +118,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except APIUnavailable:
|
||||
except AsekoAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuthCredentials:
|
||||
except AsekoInvalidCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
@ -5,34 +5,31 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioaseko import Unit, Variable
|
||||
from aioaseko import Aseko, Unit
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
|
||||
"""Class to manage fetching Aseko unit data from single endpoint."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
|
||||
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
|
||||
"""Initialize global Aseko unit data updater."""
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.name:
|
||||
name = self._unit.name
|
||||
else:
|
||||
name = f"{self._unit.type}-{self._unit.serial_number}"
|
||||
self._aseko = aseko
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=name,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(minutes=2),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Variable]:
|
||||
async def _async_update_data(self) -> dict[str, Unit]:
|
||||
"""Fetch unit data."""
|
||||
await self._unit.get_state()
|
||||
return {variable.type: variable for variable in self._unit.variables}
|
||||
units = await self._aseko.get_units()
|
||||
return {unit.serial_number: unit for unit in units}
|
||||
|
@ -3,6 +3,7 @@
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the aseko entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.type == "Remote":
|
||||
self._device_model = "ASIN Pool"
|
||||
else:
|
||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
||||
self._device_name = self._unit.name if self._unit.name else self._device_model
|
||||
|
||||
self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self._device_name,
|
||||
identifiers={(DOMAIN, str(self._unit.serial_number))},
|
||||
manufacturer="Aseko",
|
||||
model=self._device_model,
|
||||
identifiers={(DOMAIN, self.unit.serial_number)},
|
||||
serial_number=self.unit.serial_number,
|
||||
name=unit.name or unit.serial_number,
|
||||
manufacturer=(
|
||||
self.unit.brand_name.primary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
model=(
|
||||
self.unit.brand_name.secondary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unit(self) -> Unit:
|
||||
"""Return the aseko unit."""
|
||||
return self.coordinator.data[self._unit.serial_number]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.unit.serial_number in self.coordinator.data
|
||||
and self.unit.online
|
||||
)
|
||||
|
@ -1,16 +1,25 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow": {
|
||||
"water_flow_to_probes": {
|
||||
"default": "mdi:waves-arrow-right"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_temperature": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"free_chlorine": {
|
||||
"default": "mdi:flask"
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"redox": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"salinity": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"water_temperature": {
|
||||
"default": "mdi:coolant-temperature"
|
||||
"default": "mdi:pool-thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaseko"],
|
||||
"requirements": ["aioaseko==0.2.0"]
|
||||
"requirements": ["aioaseko==1.0.0"]
|
||||
}
|
||||
|
@ -2,77 +2,104 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioaseko import Unit, Variable
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
from .entity import AsekoEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AsekoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Aseko sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], StateType]
|
||||
|
||||
|
||||
SENSORS: list[AsekoSensorEntityDescription] = [
|
||||
AsekoSensorEntityDescription(
|
||||
key="airTemp",
|
||||
translation_key="air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.air_temperature,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="free_chlorine",
|
||||
translation_key="free_chlorine",
|
||||
native_unit_of_measurement="mg/l",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.cl_free,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="ph",
|
||||
device_class=SensorDeviceClass.PH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.ph,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="rx",
|
||||
translation_key="redox",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.redox,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="salinity",
|
||||
translation_key="salinity",
|
||||
native_unit_of_measurement="kg/m³",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.salinity,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="waterTemp",
|
||||
translation_key="water_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.water_temperature,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live sensors."""
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
VariableSensorEntity(unit, variable, coordinator)
|
||||
for unit, coordinator in data
|
||||
for variable in unit.variables
|
||||
AsekoSensorEntity(unit, coordinator, description)
|
||||
for description in SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
)
|
||||
|
||||
|
||||
class VariableSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of a unit variable sensor entity."""
|
||||
class AsekoSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of an Aseko unit sensor entity."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the variable sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self._variable = variable
|
||||
|
||||
translation_key = {
|
||||
"Air temp.": "air_temperature",
|
||||
"Cl free": "free_chlorine",
|
||||
"Water temp.": "water_temperature",
|
||||
}.get(self._variable.name)
|
||||
if translation_key is not None:
|
||||
self._attr_translation_key = translation_key
|
||||
else:
|
||||
self._attr_name = self._variable.name
|
||||
|
||||
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
|
||||
self._attr_native_unit_of_measurement = self._variable.unit
|
||||
|
||||
self._attr_icon = {
|
||||
"rx": "mdi:test-tube",
|
||||
"waterLevel": "mdi:waves",
|
||||
}.get(self._variable.type)
|
||||
|
||||
self._attr_device_class = {
|
||||
"airTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"waterTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"ph": SensorDeviceClass.PH,
|
||||
}.get(self._variable.type)
|
||||
entity_description: AsekoSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
variable = self.coordinator.data[self._variable.type]
|
||||
return variable.current_value
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
|
@ -26,11 +26,8 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow": {
|
||||
"name": "Water flow"
|
||||
},
|
||||
"alarm": {
|
||||
"name": "Alarm"
|
||||
"water_flow_to_probes": {
|
||||
"name": "Water flow to probes"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@ -40,6 +37,12 @@
|
||||
"free_chlorine": {
|
||||
"name": "Free chlorine"
|
||||
},
|
||||
"redox": {
|
||||
"name": "Redox potential"
|
||||
},
|
||||
"salinity": {
|
||||
"name": "Salinity"
|
||||
},
|
||||
"water_temperature": {
|
||||
"name": "Water temperature"
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import DOMAIN
|
||||
from .websocket import BangOlufsenWebsocket
|
||||
@ -48,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
model=entry.data[CONF_MODEL],
|
||||
)
|
||||
|
||||
client = MozartClient(host=entry.data[CONF_HOST])
|
||||
client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())
|
||||
|
||||
# Check API and WebSocket connection
|
||||
try:
|
||||
|
@ -14,6 +14,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@ -87,7 +88,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors={"base": _exception_map[type(error)]},
|
||||
)
|
||||
|
||||
self._client = MozartClient(self._host)
|
||||
self._client = MozartClient(
|
||||
host=self._host, ssl_context=get_default_context()
|
||||
)
|
||||
|
||||
# Try to get information from Beolink self method.
|
||||
async with self._client:
|
||||
@ -136,7 +139,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
# Check connection to ensure valid address is received
|
||||
self._client = MozartClient(self._host)
|
||||
self._client = MozartClient(self._host, ssl_context=get_default_context())
|
||||
|
||||
async with self._client:
|
||||
try:
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["mozart-api==3.4.1.8.6"],
|
||||
"requirements": ["mozart-api==3.4.1.8.8"],
|
||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||
}
|
||||
|
@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.13.6"],
|
||||
"requirements": ["pydaikin==2.13.7"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
@ -83,20 +83,38 @@ def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | Non
|
||||
return None
|
||||
|
||||
|
||||
def value_nextchange_preset(device: FritzhomeDevice) -> str:
|
||||
def value_nextchange_preset(device: FritzhomeDevice) -> str | None:
|
||||
"""Return native value for next scheduled preset sensor."""
|
||||
if not device.nextchange_endperiod:
|
||||
return None
|
||||
if device.nextchange_temperature == device.eco_temperature:
|
||||
return PRESET_ECO
|
||||
return PRESET_COMFORT
|
||||
|
||||
|
||||
def value_scheduled_preset(device: FritzhomeDevice) -> str:
|
||||
def value_scheduled_preset(device: FritzhomeDevice) -> str | None:
|
||||
"""Return native value for current scheduled preset sensor."""
|
||||
if not device.nextchange_endperiod:
|
||||
return None
|
||||
if device.nextchange_temperature == device.eco_temperature:
|
||||
return PRESET_COMFORT
|
||||
return PRESET_ECO
|
||||
|
||||
|
||||
def value_nextchange_temperature(device: FritzhomeDevice) -> float | None:
|
||||
"""Return native value for next scheduled temperature time sensor."""
|
||||
if device.nextchange_endperiod and isinstance(device.nextchange_temperature, float):
|
||||
return device.nextchange_temperature
|
||||
return None
|
||||
|
||||
|
||||
def value_nextchange_time(device: FritzhomeDevice) -> datetime | None:
|
||||
"""Return native value for next scheduled changed time sensor."""
|
||||
if device.nextchange_endperiod:
|
||||
return utc_from_timestamp(device.nextchange_endperiod)
|
||||
return None
|
||||
|
||||
|
||||
SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
FritzSensorEntityDescription(
|
||||
key="temperature",
|
||||
@ -181,7 +199,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=suitable_nextchange_temperature,
|
||||
native_value=lambda device: device.nextchange_temperature,
|
||||
native_value=value_nextchange_temperature,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="nextchange_time",
|
||||
@ -189,7 +207,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=suitable_nextchange_time,
|
||||
native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod),
|
||||
native_value=value_nextchange_time,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="nextchange_preset",
|
||||
|
@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
@ -14,25 +14,44 @@ from homeassistant.util import dt as dt_util
|
||||
def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None:
|
||||
"""Calculate due date for dailies and yesterdailies."""
|
||||
|
||||
today = to_date(last_cron)
|
||||
startdate = to_date(task["startDate"])
|
||||
if TYPE_CHECKING:
|
||||
assert today
|
||||
assert startdate
|
||||
|
||||
if task["isDue"] and not task["completed"]:
|
||||
return dt_util.as_local(datetime.datetime.fromisoformat(last_cron)).date()
|
||||
return to_date(last_cron)
|
||||
|
||||
if startdate > today:
|
||||
if task["frequency"] == "daily" or (
|
||||
task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"]
|
||||
):
|
||||
return startdate
|
||||
|
||||
if (
|
||||
task["frequency"] in ("weekly", "monthly")
|
||||
and (nextdue := to_date(task["nextDue"][0]))
|
||||
and startdate > nextdue
|
||||
):
|
||||
return to_date(task["nextDue"][1])
|
||||
|
||||
return to_date(task["nextDue"][0])
|
||||
|
||||
|
||||
def to_date(date: str) -> datetime.date | None:
|
||||
"""Convert an iso date to a datetime.date object."""
|
||||
try:
|
||||
return dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task["nextDue"][0])
|
||||
).date()
|
||||
return dt_util.as_local(datetime.datetime.fromisoformat(date)).date()
|
||||
except ValueError:
|
||||
# sometimes nextDue dates are in this format instead of iso:
|
||||
# sometimes nextDue dates are JavaScript datetime strings instead of iso:
|
||||
# "Mon May 06 2024 00:00:00 GMT+0200"
|
||||
try:
|
||||
return dt_util.as_local(
|
||||
datetime.datetime.strptime(
|
||||
task["nextDue"][0], "%a %b %d %Y %H:%M:%S %Z%z"
|
||||
)
|
||||
datetime.datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z")
|
||||
).date()
|
||||
except ValueError:
|
||||
return None
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
|
@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.56", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.57", "babel==2.15.0"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2024.8.0"]
|
||||
"requirements": ["pydrawise==2024.9.0"]
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from jvcprojector import (
|
||||
JvcProjector,
|
||||
@ -40,7 +41,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
self.device = device
|
||||
self.unique_id = format_mac(device.mac)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, str]:
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get the latest state data."""
|
||||
try:
|
||||
state = await self.device.get_state()
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==1.0.12"]
|
||||
"requirements": ["pyjvcprojector==1.1.0"]
|
||||
}
|
||||
|
@ -2,20 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from xknx.devices import Device as XknxDevice
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
from .storage.config_store import PlatformControllerBase
|
||||
from .storage.const import CONF_DEVICE_INFO
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KNXModule
|
||||
|
||||
from .storage.config_store import PlatformControllerBase
|
||||
|
||||
|
||||
class KnxUiEntityPlatformController(PlatformControllerBase):
|
||||
"""Class to manage dynamic adding and reloading of UI entities."""
|
||||
@ -93,13 +96,19 @@ class KnxYamlEntity(_KnxEntityBase):
|
||||
self._device = device
|
||||
|
||||
|
||||
class KnxUiEntity(_KnxEntityBase, ABC):
|
||||
class KnxUiEntity(_KnxEntityBase):
|
||||
"""Representation of a KNX UI entity."""
|
||||
|
||||
_attr_unique_id: str
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@abstractmethod
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
||||
self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize the UI entity."""
|
||||
self._knx_module = knx_module
|
||||
self._attr_unique_id = unique_id
|
||||
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
|
||||
self._attr_entity_category = EntityCategory(entity_category)
|
||||
if device_info := entity_config.get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
@ -20,7 +20,6 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@ -35,7 +34,6 @@ from .schema import LightSchema
|
||||
from .storage.const import (
|
||||
CONF_COLOR_TEMP_MAX,
|
||||
CONF_COLOR_TEMP_MIN,
|
||||
CONF_DEVICE_INFO,
|
||||
CONF_DPT,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
@ -554,21 +552,19 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity):
|
||||
class KnxUiLight(_KnxLight, KnxUiEntity):
|
||||
"""Representation of a KNX light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_device: XknxLight
|
||||
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: ConfigType
|
||||
) -> None:
|
||||
"""Initialize of KNX light."""
|
||||
self._knx_module = knx_module
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
self._device = _create_ui_light(
|
||||
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
|
||||
)
|
||||
self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX]
|
||||
self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN]
|
||||
|
||||
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
|
||||
self._attr_unique_id = unique_id
|
||||
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@ -38,7 +37,6 @@ from .const import (
|
||||
from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import (
|
||||
CONF_DEVICE_INFO,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
@ -133,14 +131,17 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
|
||||
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
"""Representation of a KNX switch configured from UI."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_device: XknxSwitch
|
||||
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize KNX switch."""
|
||||
self._knx_module = knx_module
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
self._device = XknxSwitch(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
@ -153,7 +154,3 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN][CONF_INVERT],
|
||||
)
|
||||
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
|
||||
self._attr_unique_id = unique_id
|
||||
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
@ -190,48 +190,56 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||
# if the mains power is off - treat it as if the HVAC mode is off
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_hvac_action = None
|
||||
return
|
||||
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode)
|
||||
)
|
||||
match system_mode_value:
|
||||
case SystemModeEnum.kAuto:
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
|
||||
self._attr_hvac_mode = HVACMode.COOL
|
||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case _:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
# running state is an optional attribute
|
||||
# which we map to hvac_action if it exists (its value is not None)
|
||||
self._attr_hvac_action = None
|
||||
if running_state_value := self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState
|
||||
):
|
||||
match running_state_value:
|
||||
case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2:
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2:
|
||||
self._attr_hvac_action = HVACAction.COOLING
|
||||
case (
|
||||
ThermostatRunningState.Fan
|
||||
| ThermostatRunningState.FanStage2
|
||||
| ThermostatRunningState.FanStage3
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
else:
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.SystemMode
|
||||
)
|
||||
)
|
||||
match system_mode_value:
|
||||
case SystemModeEnum.kAuto:
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
|
||||
self._attr_hvac_mode = HVACMode.COOL
|
||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
# running state is an optional attribute
|
||||
# which we map to hvac_action if it exists (its value is not None)
|
||||
self._attr_hvac_action = None
|
||||
if running_state_value := self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState
|
||||
):
|
||||
match running_state_value:
|
||||
case (
|
||||
ThermostatRunningState.Heat
|
||||
| ThermostatRunningState.HeatStage2
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
case (
|
||||
ThermostatRunningState.Cool
|
||||
| ThermostatRunningState.CoolStage2
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.COOLING
|
||||
case (
|
||||
ThermostatRunningState.Fan
|
||||
| ThermostatRunningState.FanStage2
|
||||
| ThermostatRunningState.FanStage3
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
|
||||
# update target temperature high/low
|
||||
supports_range = (
|
||||
self._attr_supported_features
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError
|
||||
from aiomealie import MealieAuthenticationError, MealieClient, MealieError
|
||||
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
|
||||
version = create_version(about.version)
|
||||
except MealieAuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
except MealieConnectionError as error:
|
||||
except MealieError as error:
|
||||
raise ConfigEntryNotReady(error) from error
|
||||
|
||||
if not version.valid:
|
||||
|
@ -173,7 +173,9 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
await self.device.async_set_brightness(
|
||||
round(kwargs[ATTR_BRIGHTNESS] / 2.55)
|
||||
)
|
||||
|
||||
else:
|
||||
await self.device.async_on()
|
||||
@ -194,6 +196,6 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
|
||||
|
||||
if (brightness := self.device.brightness) is not None:
|
||||
# Netatmo uses a range of [0, 100] to control brightness
|
||||
self._attr_brightness = round((brightness / 100) * 255)
|
||||
self._attr_brightness = round(brightness * 2.55)
|
||||
else:
|
||||
self._attr_brightness = None
|
||||
|
@ -96,11 +96,10 @@ class PS4Device(MediaPlayerEntity):
|
||||
self._retry = 0
|
||||
self._disconnected = False
|
||||
|
||||
@callback
|
||||
def status_callback(self) -> None:
|
||||
"""Handle status callback. Parse status."""
|
||||
self._parse_status()
|
||||
self.async_write_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def subscribe_to_protocol(self) -> None:
|
||||
@ -157,7 +156,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol
|
||||
self.subscribe_to_protocol()
|
||||
|
||||
self._parse_status()
|
||||
await self.hass.async_add_executor_job(self._parse_status)
|
||||
|
||||
def _parse_status(self) -> None:
|
||||
"""Parse status."""
|
||||
|
@ -177,8 +177,12 @@ def count_torrents_in_states(
|
||||
# When torrents are not in the returned data, there are none, return 0.
|
||||
try:
|
||||
torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents"))
|
||||
if torrents is None:
|
||||
return 0
|
||||
|
||||
if not states:
|
||||
return len(torrents)
|
||||
|
||||
return len(
|
||||
[torrent for torrent in torrents.values() if torrent.get("state") in states]
|
||||
)
|
||||
|
@ -10,9 +10,8 @@ import surepy
|
||||
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, SURE_API_TIMEOUT
|
||||
@ -27,57 +26,43 @@ USER_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
surepy_client = surepy.Surepy(
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
token = await surepy_client.sac.get_token()
|
||||
|
||||
return {CONF_TOKEN: token}
|
||||
|
||||
|
||||
class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sure Petcare."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._username: str | None = None
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
user_input[CONF_TOKEN] = info[CONF_TOKEN]
|
||||
return self.async_create_entry(
|
||||
title="Sure Petcare",
|
||||
data=user_input,
|
||||
if user_input is not None:
|
||||
client = surepy.Surepy(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
token = await client.sac.get_token()
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Sure Petcare",
|
||||
data={**user_input, CONF_TOKEN: token},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors
|
||||
@ -87,18 +72,27 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._username = entry_data[CONF_USERNAME]
|
||||
self.reauth_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
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
assert self.reauth_entry
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
user_input[CONF_USERNAME] = self._username
|
||||
client = surepy.Surepy(
|
||||
self.reauth_entry.data[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
token = await client.sac.get_token()
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
@ -107,16 +101,20 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_USERNAME].lower()
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry,
|
||||
data={
|
||||
**self.reauth_entry.data,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_TOKEN: token,
|
||||
},
|
||||
)
|
||||
if existing_entry:
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={"username": self._username},
|
||||
description_placeholders={
|
||||
"username": self.reauth_entry.data[CONF_USERNAME]
|
||||
},
|
||||
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
@ -192,3 +192,10 @@ class TeslemetryWallConnectorEntity(
|
||||
.get(self.din, {})
|
||||
.get(self.key)
|
||||
)
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
"""Return True if it exists in the wall connector coordinator data."""
|
||||
return self.key in self.coordinator.data.get("wall_connectors", {}).get(
|
||||
self.din, {}
|
||||
)
|
||||
|
@ -379,18 +379,18 @@ ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM),
|
||||
)
|
||||
|
||||
WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = (
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_state",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_fault_state",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_power",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
@ -398,8 +398,9 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="vin",
|
||||
value_fn=lambda vin: vin or "disconnected",
|
||||
),
|
||||
)
|
||||
|
||||
@ -525,13 +526,13 @@ class TeslemetryEnergyLiveSensorEntity(TeslemetryEnergyLiveEntity, SensorEntity)
|
||||
class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity):
|
||||
"""Base class for Teslemetry energy site metric sensors."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
entity_description: TeslemetrySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: TeslemetryEnergyData,
|
||||
din: str,
|
||||
description: SensorEntityDescription,
|
||||
description: TeslemetrySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
@ -543,8 +544,8 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the attributes of the sensor."""
|
||||
self._attr_available = not self.is_none
|
||||
self._attr_native_value = self._value
|
||||
if self.exists:
|
||||
self._attr_native_value = self.entity_description.value_fn(self._value)
|
||||
|
||||
|
||||
class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity):
|
||||
|
@ -420,7 +420,10 @@
|
||||
"name": "version"
|
||||
},
|
||||
"vin": {
|
||||
"name": "Vehicle"
|
||||
"name": "Vehicle",
|
||||
"state": {
|
||||
"disconnected": "Disconnected"
|
||||
}
|
||||
},
|
||||
"vpp_backup_reserve_percent": {
|
||||
"name": "VPP backup reserve"
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyTibber==0.30.1"]
|
||||
"requirements": ["pyTibber==0.30.2"]
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["holidays"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["holidays==0.56"]
|
||||
"requirements": ["holidays==0.57"]
|
||||
}
|
||||
|
@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 9
|
||||
PATCH_VERSION: Final = "2"
|
||||
PATCH_VERSION: Final = "3"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.9.2"
|
||||
version = "2024.9.3"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
|
@ -192,7 +192,7 @@ aioapcaccess==0.4.2
|
||||
aioaquacell==0.2.0
|
||||
|
||||
# homeassistant.components.aseko_pool_live
|
||||
aioaseko==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.4.0
|
||||
@ -410,7 +410,7 @@ aiowithings==3.0.3
|
||||
aioymaps==1.2.5
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.8.0
|
||||
airgradient==0.9.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
@ -1099,7 +1099,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.56
|
||||
holidays==0.57
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240909.1
|
||||
@ -1375,7 +1375,7 @@ motionblindsble==0.1.1
|
||||
motioneye-client==0.3.14
|
||||
|
||||
# homeassistant.components.bang_olufsen
|
||||
mozart-api==3.4.1.8.6
|
||||
mozart-api==3.4.1.8.8
|
||||
|
||||
# homeassistant.components.mullvad
|
||||
mullvad-api==1.0.0
|
||||
@ -1707,7 +1707,7 @@ pyRFXtrx==0.31.1
|
||||
pySDCP==1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.30.1
|
||||
pyTibber==0.30.2
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
@ -1798,7 +1798,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==1.9.73
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.13.6
|
||||
pydaikin==2.13.7
|
||||
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
@ -1819,7 +1819,7 @@ pydiscovergy==3.0.1
|
||||
pydoods==1.0.2
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2024.8.0
|
||||
pydrawise==2024.9.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==2.0.0
|
||||
@ -1954,7 +1954,7 @@ pyisy==3.1.14
|
||||
pyitachip2ir==0.0.7
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.12
|
||||
pyjvcprojector==1.1.0
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
|
@ -180,7 +180,7 @@ aioapcaccess==0.4.2
|
||||
aioaquacell==0.2.0
|
||||
|
||||
# homeassistant.components.aseko_pool_live
|
||||
aioaseko==0.2.0
|
||||
aioaseko==1.0.0
|
||||
|
||||
# homeassistant.components.asuswrt
|
||||
aioasuswrt==1.4.0
|
||||
@ -392,7 +392,7 @@ aiowithings==3.0.3
|
||||
aioymaps==1.2.5
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.8.0
|
||||
airgradient==0.9.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
@ -922,7 +922,7 @@ hole==0.8.0
|
||||
|
||||
# homeassistant.components.holiday
|
||||
# homeassistant.components.workday
|
||||
holidays==0.56
|
||||
holidays==0.57
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240909.1
|
||||
@ -1141,7 +1141,7 @@ motionblindsble==0.1.1
|
||||
motioneye-client==0.3.14
|
||||
|
||||
# homeassistant.components.bang_olufsen
|
||||
mozart-api==3.4.1.8.6
|
||||
mozart-api==3.4.1.8.8
|
||||
|
||||
# homeassistant.components.mullvad
|
||||
mullvad-api==1.0.0
|
||||
@ -1381,7 +1381,7 @@ pyElectra==1.2.4
|
||||
pyRFXtrx==0.31.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.30.1
|
||||
pyTibber==0.30.2
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
@ -1445,7 +1445,7 @@ pycoolmasternet-async==0.2.2
|
||||
pycsspeechtts==1.0.8
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.13.6
|
||||
pydaikin==2.13.7
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==116
|
||||
@ -1457,7 +1457,7 @@ pydexcom==0.2.3
|
||||
pydiscovergy==3.0.1
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
pydrawise==2024.8.0
|
||||
pydrawise==2024.9.0
|
||||
|
||||
# homeassistant.components.android_ip_webcam
|
||||
pydroid-ipcam==2.0.0
|
||||
@ -1559,7 +1559,7 @@ pyiss==1.0.1
|
||||
pyisy==3.1.14
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.12
|
||||
pyjvcprojector==1.1.0
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
|
@ -305,7 +305,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '48.0',
|
||||
'state': '47.0',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[indoor][sensor.airgradient_led_bar_brightness-entry]
|
||||
@ -912,7 +912,7 @@
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': '27.96',
|
||||
'state': '22.17',
|
||||
})
|
||||
# ---
|
||||
# name: test_all_entities[indoor][sensor.airgradient_voc_index-entry]
|
||||
|
20
tests/components/aseko_pool_live/conftest.py
Normal file
20
tests/components/aseko_pool_live/conftest.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""Aseko Pool Live conftest."""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from aioaseko import User
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def user() -> User:
|
||||
"""Aseko User fixture."""
|
||||
return User(
|
||||
user_id="a_user_id",
|
||||
created_at=datetime.now(),
|
||||
updated_at=datetime.now(),
|
||||
name="John",
|
||||
surname="Doe",
|
||||
language="any_language",
|
||||
is_active=True,
|
||||
)
|
@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from aioaseko import AccountInfo, APIUnavailable, InvalidAuthCredentials
|
||||
from aioaseko import AsekoAPIError, AsekoInvalidCredentials, User
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -23,7 +23,7 @@ async def test_async_step_user_form(hass: HomeAssistant) -> None:
|
||||
assert result["errors"] == {}
|
||||
|
||||
|
||||
async def test_async_step_user_success(hass: HomeAssistant) -> None:
|
||||
async def test_async_step_user_success(hass: HomeAssistant, user: User) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
@ -31,8 +31,8 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.WebAccount.login",
|
||||
return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"),
|
||||
"homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
|
||||
return_value=user,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.aseko_pool_live.async_setup_entry",
|
||||
@ -60,13 +60,13 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("error_web", "reason"),
|
||||
[
|
||||
(APIUnavailable, "cannot_connect"),
|
||||
(InvalidAuthCredentials, "invalid_auth"),
|
||||
(AsekoAPIError, "cannot_connect"),
|
||||
(AsekoInvalidCredentials, "invalid_auth"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_async_step_user_exception(
|
||||
hass: HomeAssistant, error_web: Exception, reason: str
|
||||
hass: HomeAssistant, user: User, error_web: Exception, reason: str
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -74,8 +74,8 @@ async def test_async_step_user_exception(
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.WebAccount.login",
|
||||
return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"),
|
||||
"homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
|
||||
return_value=user,
|
||||
side_effect=error_web,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@ -93,13 +93,13 @@ async def test_async_step_user_exception(
|
||||
@pytest.mark.parametrize(
|
||||
("error_web", "reason"),
|
||||
[
|
||||
(APIUnavailable, "cannot_connect"),
|
||||
(InvalidAuthCredentials, "invalid_auth"),
|
||||
(AsekoAPIError, "cannot_connect"),
|
||||
(AsekoInvalidCredentials, "invalid_auth"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_get_account_info_exceptions(
|
||||
hass: HomeAssistant, error_web: Exception, reason: str
|
||||
hass: HomeAssistant, user: User, error_web: Exception, reason: str
|
||||
) -> None:
|
||||
"""Test we handle config flow exceptions."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@ -107,8 +107,8 @@ async def test_get_account_info_exceptions(
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.WebAccount.login",
|
||||
return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"),
|
||||
"homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
|
||||
return_value=user,
|
||||
side_effect=error_web,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
@ -123,7 +123,7 @@ async def test_get_account_info_exceptions(
|
||||
assert result2["errors"] == {"base": reason}
|
||||
|
||||
|
||||
async def test_async_step_reauth_success(hass: HomeAssistant) -> None:
|
||||
async def test_async_step_reauth_success(hass: HomeAssistant, user: User) -> None:
|
||||
"""Test successful reauthentication."""
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
@ -139,10 +139,16 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None:
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {}
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.WebAccount.login",
|
||||
return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"),
|
||||
) as mock_setup_entry:
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
|
||||
return_value=user,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.aseko_pool_live.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_EMAIL: "aseko@example.com", CONF_PASSWORD: "passw0rd"},
|
||||
@ -156,13 +162,13 @@ async def test_async_step_reauth_success(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("error_web", "reason"),
|
||||
[
|
||||
(APIUnavailable, "cannot_connect"),
|
||||
(InvalidAuthCredentials, "invalid_auth"),
|
||||
(AsekoAPIError, "cannot_connect"),
|
||||
(AsekoInvalidCredentials, "invalid_auth"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_async_step_reauth_exception(
|
||||
hass: HomeAssistant, error_web: Exception, reason: str
|
||||
hass: HomeAssistant, user: User, error_web: Exception, reason: str
|
||||
) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
@ -176,8 +182,8 @@ async def test_async_step_reauth_exception(
|
||||
result = await mock_entry.start_reauth_flow(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.aseko_pool_live.config_flow.WebAccount.login",
|
||||
return_value=AccountInfo("aseko@example.com", "a_user_id", "any_language"),
|
||||
"homeassistant.components.aseko_pool_live.config_flow.Aseko.login",
|
||||
return_value=user,
|
||||
side_effect=error_web,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
from unittest.mock import Mock
|
||||
|
||||
from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO
|
||||
from homeassistant.components.fritzbox.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@ -110,9 +109,7 @@ class FritzDeviceClimateMock(FritzEntityBaseMock):
|
||||
target_temperature = 19.5
|
||||
window_open = "fake_window"
|
||||
nextchange_temperature = 22.0
|
||||
nextchange_endperiod = 0
|
||||
nextchange_preset = PRESET_COMFORT
|
||||
scheduled_preset = PRESET_ECO
|
||||
nextchange_endperiod = 1726855200
|
||||
|
||||
|
||||
class FritzDeviceClimateWithoutTempSensorMock(FritzDeviceClimateMock):
|
||||
|
@ -123,7 +123,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_change_time"
|
||||
)
|
||||
assert state
|
||||
assert state.state == "1970-01-01T00:00:00+00:00"
|
||||
assert state.state == "2024-09-20T18:00:00+00:00"
|
||||
assert (
|
||||
state.attributes[ATTR_FRIENDLY_NAME]
|
||||
== f"{CONF_FAKE_NAME} Next scheduled change time"
|
||||
|
@ -3,8 +3,10 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from requests.exceptions import HTTPError
|
||||
|
||||
from homeassistant.components.climate import PRESET_COMFORT, PRESET_ECO
|
||||
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN, SensorStateClass
|
||||
from homeassistant.const import (
|
||||
@ -12,6 +14,7 @@ from homeassistant.const import (
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_DEVICES,
|
||||
PERCENTAGE,
|
||||
STATE_UNKNOWN,
|
||||
EntityCategory,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
@ -19,7 +22,12 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import FritzDeviceSensorMock, set_devices, setup_config_entry
|
||||
from . import (
|
||||
FritzDeviceClimateMock,
|
||||
FritzDeviceSensorMock,
|
||||
set_devices,
|
||||
setup_config_entry,
|
||||
)
|
||||
from .const import CONF_FAKE_NAME, MOCK_CONFIG
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
@ -132,3 +140,55 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None:
|
||||
|
||||
state = hass.states.get(f"{DOMAIN}.new_device_temperature")
|
||||
assert state
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("next_changes", "expected_states"),
|
||||
[
|
||||
(
|
||||
[0, 16],
|
||||
[STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN],
|
||||
),
|
||||
(
|
||||
[0, 22],
|
||||
[STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN, STATE_UNKNOWN],
|
||||
),
|
||||
(
|
||||
[1726855200, 16.0],
|
||||
["2024-09-20T18:00:00+00:00", "16.0", PRESET_ECO, PRESET_COMFORT],
|
||||
),
|
||||
(
|
||||
[1726855200, 22.0],
|
||||
["2024-09-20T18:00:00+00:00", "22.0", PRESET_COMFORT, PRESET_ECO],
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_next_change_sensors(
|
||||
hass: HomeAssistant, fritz: Mock, next_changes: list, expected_states: list
|
||||
) -> None:
|
||||
"""Test next change sensors."""
|
||||
device = FritzDeviceClimateMock()
|
||||
device.nextchange_endperiod = next_changes[0]
|
||||
device.nextchange_temperature = next_changes[1]
|
||||
|
||||
assert await setup_config_entry(
|
||||
hass, MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0], ENTITY_ID, device, fritz
|
||||
)
|
||||
|
||||
base_name = f"{DOMAIN}.{CONF_FAKE_NAME}"
|
||||
|
||||
state = hass.states.get(f"{base_name}_next_scheduled_change_time")
|
||||
assert state
|
||||
assert state.state == expected_states[0]
|
||||
|
||||
state = hass.states.get(f"{base_name}_next_scheduled_temperature")
|
||||
assert state
|
||||
assert state.state == expected_states[1]
|
||||
|
||||
state = hass.states.get(f"{base_name}_next_scheduled_preset")
|
||||
assert state
|
||||
assert state.state == expected_states[2]
|
||||
|
||||
state = hass.states.get(f"{base_name}_current_scheduled_preset")
|
||||
assert state
|
||||
assert state.state == expected_states[3]
|
||||
|
@ -18,22 +18,22 @@ async def test_something(hass, knx):
|
||||
|
||||
## Asserting outgoing telegrams
|
||||
|
||||
All outgoing telegrams are pushed to an assertion queue. Assert them in order they were sent.
|
||||
All outgoing telegrams are appended to an assertion list. Assert them in order they were sent or pass `ignore_order=True` to the assertion method.
|
||||
|
||||
- `knx.assert_no_telegram`
|
||||
Asserts that no telegram was sent (assertion queue is empty).
|
||||
Asserts that no telegram was sent (assertion list is empty).
|
||||
- `knx.assert_telegram_count(count: int)`
|
||||
Asserts that `count` telegrams were sent.
|
||||
- `knx.assert_read(group_address: str, response: int | tuple[int, ...] | None = None)`
|
||||
- `knx.assert_read(group_address: str, response: int | tuple[int, ...] | None = None, ignore_order: bool = False)`
|
||||
Asserts that a GroupValueRead telegram was sent to `group_address`.
|
||||
The telegram will be removed from the assertion queue.
|
||||
The telegram will be removed from the assertion list.
|
||||
Optionally inject incoming GroupValueResponse telegram after reception to clear the value reader waiting task. This can also be done manually with `knx.receive_response`.
|
||||
- `knx.assert_response(group_address: str, payload: int | tuple[int, ...])`
|
||||
- `knx.assert_response(group_address: str, payload: int | tuple[int, ...], ignore_order: bool = False)`
|
||||
Asserts that a GroupValueResponse telegram with `payload` was sent to `group_address`.
|
||||
The telegram will be removed from the assertion queue.
|
||||
- `knx.assert_write(group_address: str, payload: int | tuple[int, ...])`
|
||||
The telegram will be removed from the assertion list.
|
||||
- `knx.assert_write(group_address: str, payload: int | tuple[int, ...], ignore_order: bool = False)`
|
||||
Asserts that a GroupValueWrite telegram with `payload` was sent to `group_address`.
|
||||
The telegram will be removed from the assertion queue.
|
||||
The telegram will be removed from the assertion list.
|
||||
|
||||
Change some states or call some services and assert outgoing telegrams.
|
||||
|
||||
|
@ -57,9 +57,9 @@ class KNXTestKit:
|
||||
self.hass: HomeAssistant = hass
|
||||
self.mock_config_entry: MockConfigEntry = mock_config_entry
|
||||
self.xknx: XKNX
|
||||
# outgoing telegrams will be put in the Queue instead of sent to the interface
|
||||
# outgoing telegrams will be put in the List instead of sent to the interface
|
||||
# telegrams to an InternalGroupAddress won't be queued here
|
||||
self._outgoing_telegrams: asyncio.Queue = asyncio.Queue()
|
||||
self._outgoing_telegrams: list[Telegram] = []
|
||||
|
||||
def assert_state(self, entity_id: str, state: str, **attributes) -> None:
|
||||
"""Assert the state of an entity."""
|
||||
@ -76,7 +76,7 @@ class KNXTestKit:
|
||||
async def patch_xknx_start():
|
||||
"""Patch `xknx.start` for unittests."""
|
||||
self.xknx.cemi_handler.send_telegram = AsyncMock(
|
||||
side_effect=self._outgoing_telegrams.put
|
||||
side_effect=self._outgoing_telegrams.append
|
||||
)
|
||||
# after XKNX.__init__() to not overwrite it by the config entry again
|
||||
# before StateUpdater starts to avoid slow down of tests
|
||||
@ -117,24 +117,22 @@ class KNXTestKit:
|
||||
########################
|
||||
|
||||
def _list_remaining_telegrams(self) -> str:
|
||||
"""Return a string containing remaining outgoing telegrams in test Queue. One per line."""
|
||||
remaining_telegrams = []
|
||||
while not self._outgoing_telegrams.empty():
|
||||
remaining_telegrams.append(self._outgoing_telegrams.get_nowait())
|
||||
return "\n".join(map(str, remaining_telegrams))
|
||||
"""Return a string containing remaining outgoing telegrams in test List."""
|
||||
return "\n".join(map(str, self._outgoing_telegrams))
|
||||
|
||||
async def assert_no_telegram(self) -> None:
|
||||
"""Assert if every telegram in test Queue was checked."""
|
||||
"""Assert if every telegram in test List was checked."""
|
||||
await self.hass.async_block_till_done()
|
||||
assert self._outgoing_telegrams.empty(), (
|
||||
f"Found remaining unasserted Telegrams: {self._outgoing_telegrams.qsize()}\n"
|
||||
remaining_telegram_count = len(self._outgoing_telegrams)
|
||||
assert not remaining_telegram_count, (
|
||||
f"Found remaining unasserted Telegrams: {remaining_telegram_count}\n"
|
||||
f"{self._list_remaining_telegrams()}"
|
||||
)
|
||||
|
||||
async def assert_telegram_count(self, count: int) -> None:
|
||||
"""Assert outgoing telegram count in test Queue."""
|
||||
"""Assert outgoing telegram count in test List."""
|
||||
await self.hass.async_block_till_done()
|
||||
actual_count = self._outgoing_telegrams.qsize()
|
||||
actual_count = len(self._outgoing_telegrams)
|
||||
assert actual_count == count, (
|
||||
f"Outgoing telegrams: {actual_count} - Expected: {count}\n"
|
||||
f"{self._list_remaining_telegrams()}"
|
||||
@ -149,52 +147,79 @@ class KNXTestKit:
|
||||
group_address: str,
|
||||
payload: int | tuple[int, ...] | None,
|
||||
apci_type: type[APCI],
|
||||
ignore_order: bool = False,
|
||||
) -> None:
|
||||
"""Assert outgoing telegram. One by one in timely order."""
|
||||
"""Assert outgoing telegram. Optionally in timely order."""
|
||||
await self.xknx.telegrams.join()
|
||||
try:
|
||||
telegram = self._outgoing_telegrams.get_nowait()
|
||||
except asyncio.QueueEmpty as err:
|
||||
if not self._outgoing_telegrams:
|
||||
raise AssertionError(
|
||||
f"No Telegram found. Expected: {apci_type.__name__} -"
|
||||
f" {group_address} - {payload}"
|
||||
) from err
|
||||
)
|
||||
_expected_ga = GroupAddress(group_address)
|
||||
|
||||
if ignore_order:
|
||||
for telegram in self._outgoing_telegrams:
|
||||
if (
|
||||
telegram.destination_address == _expected_ga
|
||||
and isinstance(telegram.payload, apci_type)
|
||||
and (payload is None or telegram.payload.value.value == payload)
|
||||
):
|
||||
self._outgoing_telegrams.remove(telegram)
|
||||
return
|
||||
raise AssertionError(
|
||||
f"Telegram not found. Expected: {apci_type.__name__} -"
|
||||
f" {group_address} - {payload}"
|
||||
f"\nUnasserted telegrams:\n{self._list_remaining_telegrams()}"
|
||||
)
|
||||
|
||||
telegram = self._outgoing_telegrams.pop(0)
|
||||
assert isinstance(
|
||||
telegram.payload, apci_type
|
||||
), f"APCI type mismatch in {telegram} - Expected: {apci_type.__name__}"
|
||||
|
||||
assert (
|
||||
str(telegram.destination_address) == group_address
|
||||
telegram.destination_address == _expected_ga
|
||||
), f"Group address mismatch in {telegram} - Expected: {group_address}"
|
||||
|
||||
if payload is not None:
|
||||
assert (
|
||||
telegram.payload.value.value == payload # type: ignore[attr-defined]
|
||||
), f"Payload mismatch in {telegram} - Expected: {payload}"
|
||||
|
||||
async def assert_read(
|
||||
self, group_address: str, response: int | tuple[int, ...] | None = None
|
||||
self,
|
||||
group_address: str,
|
||||
response: int | tuple[int, ...] | None = None,
|
||||
ignore_order: bool = False,
|
||||
) -> None:
|
||||
"""Assert outgoing GroupValueRead telegram. One by one in timely order.
|
||||
"""Assert outgoing GroupValueRead telegram. Optionally in timely order.
|
||||
|
||||
Optionally inject incoming GroupValueResponse telegram after reception.
|
||||
"""
|
||||
await self.assert_telegram(group_address, None, GroupValueRead)
|
||||
await self.assert_telegram(group_address, None, GroupValueRead, ignore_order)
|
||||
if response is not None:
|
||||
await self.receive_response(group_address, response)
|
||||
|
||||
async def assert_response(
|
||||
self, group_address: str, payload: int | tuple[int, ...]
|
||||
self,
|
||||
group_address: str,
|
||||
payload: int | tuple[int, ...],
|
||||
ignore_order: bool = False,
|
||||
) -> None:
|
||||
"""Assert outgoing GroupValueResponse telegram. One by one in timely order."""
|
||||
await self.assert_telegram(group_address, payload, GroupValueResponse)
|
||||
"""Assert outgoing GroupValueResponse telegram. Optionally in timely order."""
|
||||
await self.assert_telegram(
|
||||
group_address, payload, GroupValueResponse, ignore_order
|
||||
)
|
||||
|
||||
async def assert_write(
|
||||
self, group_address: str, payload: int | tuple[int, ...]
|
||||
self,
|
||||
group_address: str,
|
||||
payload: int | tuple[int, ...],
|
||||
ignore_order: bool = False,
|
||||
) -> None:
|
||||
"""Assert outgoing GroupValueWrite telegram. One by one in timely order."""
|
||||
await self.assert_telegram(group_address, payload, GroupValueWrite)
|
||||
"""Assert outgoing GroupValueWrite telegram. Optionally in timely order."""
|
||||
await self.assert_telegram(
|
||||
group_address, payload, GroupValueWrite, ignore_order
|
||||
)
|
||||
|
||||
####################
|
||||
# Incoming telegrams
|
||||
|
@ -23,7 +23,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"light": {}
|
||||
"light": {
|
||||
"knx_es_01J85ZKTFHSZNG4X9DYBE592TF": {
|
||||
"entity": {
|
||||
"name": "test",
|
||||
"device_info": null,
|
||||
"entity_category": "config"
|
||||
},
|
||||
"knx": {
|
||||
"color_temp_min": 2700,
|
||||
"color_temp_max": 6000,
|
||||
"_light_color_mode_schema": "default",
|
||||
"ga_switch": {
|
||||
"write": "1/1/21",
|
||||
"state": "1/0/21",
|
||||
"passive": []
|
||||
},
|
||||
"sync_state": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,8 @@ async def test_remove_device(
|
||||
await knx.setup_integration({})
|
||||
client = await hass_ws_client(hass)
|
||||
|
||||
await knx.assert_read("1/0/45", response=True)
|
||||
await knx.assert_read("1/0/21", response=True, ignore_order=True) # test light
|
||||
await knx.assert_read("1/0/45", response=True, ignore_order=True) # test switch
|
||||
|
||||
assert hass_storage[KNX_CONFIG_STORAGE_KEY]["data"]["entities"].get("switch")
|
||||
test_device = device_registry.async_get_device(
|
||||
|
@ -19,8 +19,9 @@ from homeassistant.components.light import (
|
||||
ATTR_RGBW_COLOR,
|
||||
ColorMode,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import CONF_NAME, STATE_OFF, STATE_ON, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import KnxEntityGenerator
|
||||
from .conftest import KNXTestKit
|
||||
@ -1159,7 +1160,7 @@ async def test_light_ui_create(
|
||||
knx: KNXTestKit,
|
||||
create_ui_entity: KnxEntityGenerator,
|
||||
) -> None:
|
||||
"""Test creating a switch."""
|
||||
"""Test creating a light."""
|
||||
await knx.setup_integration({})
|
||||
await create_ui_entity(
|
||||
platform=Platform.LIGHT,
|
||||
@ -1192,7 +1193,7 @@ async def test_light_ui_color_temp(
|
||||
color_temp_mode: str,
|
||||
raw_ct: tuple[int, ...],
|
||||
) -> None:
|
||||
"""Test creating a switch."""
|
||||
"""Test creating a color-temp light."""
|
||||
await knx.setup_integration({})
|
||||
await create_ui_entity(
|
||||
platform=Platform.LIGHT,
|
||||
@ -1218,3 +1219,23 @@ async def test_light_ui_color_temp(
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state is STATE_ON
|
||||
assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1)
|
||||
|
||||
|
||||
async def test_light_ui_load(
|
||||
hass: HomeAssistant,
|
||||
knx: KNXTestKit,
|
||||
load_config_store: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test loading a light from storage."""
|
||||
await knx.setup_integration({})
|
||||
|
||||
await knx.assert_read("1/0/21", response=True, ignore_order=True)
|
||||
# unrelated switch in config store
|
||||
await knx.assert_read("1/0/45", response=True, ignore_order=True)
|
||||
|
||||
state = hass.states.get("light.test")
|
||||
assert state.state is STATE_ON
|
||||
|
||||
entity = entity_registry.async_get("light.test")
|
||||
assert entity.entity_category is EntityCategory.CONFIG
|
||||
|
@ -6,6 +6,7 @@ from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.surepetcare.const import DOMAIN
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
@ -24,7 +25,7 @@ async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> N
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
assert not result["errors"]
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.surepetcare.async_setup_entry",
|
||||
@ -146,11 +147,17 @@ async def test_flow_entry_already_exists(
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_reauthentication(hass: HomeAssistant) -> None:
|
||||
async def test_reauthentication(
|
||||
hass: HomeAssistant, surepetcare: NonCallableMagicMock
|
||||
) -> None:
|
||||
"""Test surepetcare reauthentication."""
|
||||
old_entry = MockConfigEntry(
|
||||
domain="surepetcare",
|
||||
data=INPUT_DATA,
|
||||
data={
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
CONF_TOKEN: "token",
|
||||
},
|
||||
unique_id="test-username",
|
||||
)
|
||||
old_entry.add_to_hass(hass)
|
||||
@ -161,19 +168,23 @@ async def test_reauthentication(hass: HomeAssistant) -> None:
|
||||
assert result["errors"] == {}
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token",
|
||||
return_value={"token": "token"},
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"password": "test-password"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
surepetcare.get_token.return_value = "token2"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{"password": "test-password2"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reauth_successful"
|
||||
|
||||
assert old_entry.data == {
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password2",
|
||||
CONF_TOKEN: "token2",
|
||||
}
|
||||
|
||||
|
||||
async def test_reauthentication_failure(hass: HomeAssistant) -> None:
|
||||
"""Test surepetcare reauthentication failure."""
|
||||
|
Loading…
x
Reference in New Issue
Block a user