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