forked from home-assistant/core
Compare commits
89 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 06ea3a3014 | |||
| 59ecd47374 | |||
| 95053f7114 | |||
| 4949727cd5 | |||
| 08b0064ce7 | |||
| c9571126a3 | |||
| 06d825d6c8 | |||
| 36e6ab4af8 | |||
| ccec85f047 | |||
| e8a5a75e96 | |||
| 4eb1fca68e | |||
| fba24b8ead | |||
| edfb9f3f6b | |||
| 2322d071e4 | |||
| 7658ed8eaa | |||
| c81f280bc1 | |||
| 6e36febd37 | |||
| b38c193fe4 | |||
| b336cae118 | |||
| 991114eb7f | |||
| d924fc5967 | |||
| c64222de4f | |||
| b69b5aa82a | |||
| 861fcbe598 | |||
| d259055af0 | |||
| fae26ee5da | |||
| c4eca4469f | |||
| d91cc96cd2 | |||
| 0b226c1868 | |||
| 359f61e55a | |||
| 16e049b7fa | |||
| 8a6eec925f | |||
| f365995c8a | |||
| 20ded56c99 | |||
| 4583e070df | |||
| d4be1f3666 | |||
| 06d4b3281b | |||
| 1dcd5471a0 | |||
| d0b6ef877e | |||
| 1e63b956f5 | |||
| 7734bdfdab | |||
| 7eb9036cbb | |||
| 6b2526ddbd | |||
| 0b1a898c7c | |||
| fe247a60ef | |||
| 17402848f2 | |||
| e6b4c2e700 | |||
| e7c48d5870 | |||
| 781342be40 | |||
| 73b26407f6 | |||
| b1d691178e | |||
| dc189e1d58 | |||
| 444560543c | |||
| ed2d321746 | |||
| b50d8fca16 | |||
| 1c7c6d6592 | |||
| 5cf89bf2bb | |||
| 0b95cf1251 | |||
| a3f42e36ac | |||
| 973e43ae6a | |||
| e80e189e6b | |||
| 27dc2e1b9d | |||
| edb7c76caa | |||
| 7859d31ca0 | |||
| 6c640d2abe | |||
| 61ee3a9412 | |||
| 4ed18495f3 | |||
| 5c8b2cde92 | |||
| 5c2073481d | |||
| 84c204a7b3 | |||
| 6c15f251c6 | |||
| 48fcf58eb9 | |||
| 3c13f4b4cc | |||
| a14826d75e | |||
| 36ec1b33fe | |||
| 84a0a28be2 | |||
| ac19ee3e2e | |||
| 438af042ed | |||
| 122f11c790 | |||
| de99dfef4e | |||
| bcdc3563a5 | |||
| 9ef0a1f0a2 | |||
| d0629d4e66 | |||
| 65e98eab9c | |||
| a0d9764443 | |||
| 8293f270df | |||
| 516f3295bf | |||
| 94516de724 | |||
| ae4fc9504a |
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.8.0"],
|
||||
"requirements": ["airgradient==0.9.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -262,7 +262,7 @@ class Airtouch5AC(Airtouch5ClimateEntity):
|
||||
_LOGGER.debug("Argument `temperature` is missing in set_temperature")
|
||||
return
|
||||
|
||||
await self._control(temp=temp)
|
||||
await self._control(setpoint=SetpointControl.CHANGE_SETPOINT, temp=temp)
|
||||
|
||||
|
||||
class Airtouch5Zone(Airtouch5ClimateEntity):
|
||||
|
||||
@@ -8,7 +8,7 @@ LOGGER = logging.getLogger(__package__)
|
||||
CONF_RECOMMENDED = "recommended"
|
||||
CONF_PROMPT = "prompt"
|
||||
CONF_CHAT_MODEL = "chat_model"
|
||||
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
|
||||
RECOMMENDED_CHAT_MODEL = "claude-3-haiku-20240307"
|
||||
CONF_MAX_TOKENS = "max_tokens"
|
||||
RECOMMENDED_MAX_TOKENS = 1024
|
||||
CONF_TEMPERATURE = "temperature"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyatv", "srptools"],
|
||||
"requirements": ["pyatv==0.15.0"],
|
||||
"requirements": ["pyatv==0.15.1"],
|
||||
"zeroconf": [
|
||||
"_mediaremotetv._tcp.local.",
|
||||
"_companion-link._tcp.local.",
|
||||
|
||||
@@ -36,10 +36,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
max_power = (await self.api.get_device_info()).maxPower
|
||||
device_info = await self.api.get_device_info()
|
||||
except (ConnectionError, TimeoutError):
|
||||
raise UpdateFailed from None
|
||||
self.api.max_power = max_power
|
||||
self.api.max_power = device_info.maxPower
|
||||
self.api.min_power = device_info.minPower
|
||||
|
||||
async def _async_update_data(self) -> ApSystemsSensorData:
|
||||
output_data = await self.api.get_output_data()
|
||||
|
||||
@@ -26,7 +26,6 @@ async def async_setup_entry(
|
||||
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
"""Base sensor to be used with description."""
|
||||
|
||||
_attr_native_min_value = 30
|
||||
_attr_native_step = 1
|
||||
_attr_device_class = NumberDeviceClass.POWER
|
||||
_attr_mode = NumberMode.BOX
|
||||
@@ -42,6 +41,7 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
|
||||
self._api = data.coordinator.api
|
||||
self._attr_unique_id = f"{data.device_id}_output_limit"
|
||||
self._attr_native_max_value = data.coordinator.api.max_power
|
||||
self._attr_native_min_value = data.coordinator.api.min_power
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Set the state with the value fetched from the inverter."""
|
||||
|
||||
@@ -56,7 +56,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
refresh_token = await api.authenticate(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except ApiException:
|
||||
except (ApiException, TimeoutError):
|
||||
errors["base"] = "cannot_connect"
|
||||
except AuthenticationFailed:
|
||||
errors["base"] = "invalid_auth"
|
||||
|
||||
@@ -56,7 +56,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
so entities can quickly look up their data.
|
||||
"""
|
||||
|
||||
async with asyncio.timeout(10):
|
||||
async with asyncio.timeout(30):
|
||||
# Check if the refresh token is expired
|
||||
expiry_time = (
|
||||
self.refresh_token_creation_time
|
||||
@@ -72,7 +72,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
|
||||
softeners = await self.aquacell_api.get_all_softeners()
|
||||
except AuthenticationFailed as err:
|
||||
raise ConfigEntryError from err
|
||||
except AquacellApiException as err:
|
||||
except (AquacellApiException, TimeoutError) as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
return {softener.dsn: softener for softener in softeners}
|
||||
|
||||
@@ -4,13 +4,12 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, MobileAccount
|
||||
from aioaseko import Aseko, AsekoNotLoggedIn
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
@@ -22,28 +21,17 @@ PLATFORMS: list[str] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Aseko Pool Live from a config entry."""
|
||||
account = MobileAccount(
|
||||
async_get_clientsession(hass),
|
||||
username=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
)
|
||||
aseko = Aseko(entry.data[CONF_EMAIL], entry.data[CONF_PASSWORD])
|
||||
|
||||
try:
|
||||
units = await account.get_units()
|
||||
except InvalidAuthCredentials as err:
|
||||
await aseko.login()
|
||||
except AsekoNotLoggedIn as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except APIUnavailable as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = []
|
||||
|
||||
for unit in units:
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, unit)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id].append((unit, coordinator))
|
||||
|
||||
coordinator = AsekoDataUpdateCoordinator(hass, aseko)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -51,7 +39,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from dataclasses import dataclass
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
@@ -25,26 +24,14 @@ from .entity import AsekoEntity
|
||||
class AsekoBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Describes an Aseko binary sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], bool]
|
||||
value_fn: Callable[[Unit], bool | None]
|
||||
|
||||
|
||||
UNIT_BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
BINARY_SENSORS: tuple[AsekoBinarySensorEntityDescription, ...] = (
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="water_flow",
|
||||
translation_key="water_flow",
|
||||
value_fn=lambda unit: unit.water_flow,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_alarm",
|
||||
translation_key="alarm",
|
||||
value_fn=lambda unit: unit.has_alarm,
|
||||
device_class=BinarySensorDeviceClass.SAFETY,
|
||||
),
|
||||
AsekoBinarySensorEntityDescription(
|
||||
key="has_error",
|
||||
translation_key="error",
|
||||
value_fn=lambda unit: unit.has_error,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
translation_key="water_flow_to_probes",
|
||||
value_fn=lambda unit: unit.water_flow_to_probes,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -55,33 +42,22 @@ async def async_setup_entry(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live binary sensors."""
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
AsekoUnitBinarySensorEntity(unit, coordinator, description)
|
||||
for unit, coordinator in data
|
||||
for description in UNIT_BINARY_SENSORS
|
||||
AsekoBinarySensorEntity(unit, coordinator, description)
|
||||
for description in BINARY_SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
)
|
||||
|
||||
|
||||
class AsekoUnitBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of a unit water flow binary sensor entity."""
|
||||
class AsekoBinarySensorEntity(AsekoEntity, BinarySensorEntity):
|
||||
"""Representation of an Aseko binary sensor entity."""
|
||||
|
||||
entity_description: AsekoBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
entity_description: AsekoBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the unit binary sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._attr_unique_id = f"{self._unit.serial_number}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self._unit)
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
|
||||
@@ -6,12 +6,11 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aioaseko import APIUnavailable, InvalidAuthCredentials, WebAccount
|
||||
from aioaseko import Aseko, AsekoAPIError, AsekoInvalidCredentials
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -34,15 +33,12 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def get_account_info(self, email: str, password: str) -> dict:
|
||||
"""Get account info from the mobile API and the web API."""
|
||||
session = async_get_clientsession(self.hass)
|
||||
|
||||
web_account = WebAccount(session, email, password)
|
||||
web_account_info = await web_account.login()
|
||||
|
||||
aseko = Aseko(email, password)
|
||||
user = await aseko.login()
|
||||
return {
|
||||
CONF_EMAIL: email,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_UNIQUE_ID: web_account_info.user_id,
|
||||
CONF_UNIQUE_ID: user.user_id,
|
||||
}
|
||||
|
||||
async def async_step_user(
|
||||
@@ -58,9 +54,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except APIUnavailable:
|
||||
except AsekoAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuthCredentials:
|
||||
except AsekoInvalidCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
@@ -122,9 +118,9 @@ class AsekoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
info = await self.get_account_info(
|
||||
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
|
||||
)
|
||||
except APIUnavailable:
|
||||
except AsekoAPIError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuthCredentials:
|
||||
except AsekoInvalidCredentials:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -5,34 +5,31 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aioaseko import Unit, Variable
|
||||
from aioaseko import Aseko, Unit
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Variable]]):
|
||||
class AsekoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Unit]]):
|
||||
"""Class to manage fetching Aseko unit data from single endpoint."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, unit: Unit) -> None:
|
||||
def __init__(self, hass: HomeAssistant, aseko: Aseko) -> None:
|
||||
"""Initialize global Aseko unit data updater."""
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.name:
|
||||
name = self._unit.name
|
||||
else:
|
||||
name = f"{self._unit.type}-{self._unit.serial_number}"
|
||||
self._aseko = aseko
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=name,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(minutes=2),
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Variable]:
|
||||
async def _async_update_data(self) -> dict[str, Unit]:
|
||||
"""Fetch unit data."""
|
||||
await self._unit.get_state()
|
||||
return {variable.type: variable for variable in self._unit.variables}
|
||||
units = await self._aseko.get_units()
|
||||
return {unit.serial_number: unit for unit in units}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -14,20 +15,44 @@ class AsekoEntity(CoordinatorEntity[AsekoDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, unit: Unit, coordinator: AsekoDataUpdateCoordinator) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
unit: Unit,
|
||||
coordinator: AsekoDataUpdateCoordinator,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the aseko entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._unit = unit
|
||||
|
||||
if self._unit.type == "Remote":
|
||||
self._device_model = "ASIN Pool"
|
||||
else:
|
||||
self._device_model = f"ASIN AQUA {self._unit.type}"
|
||||
self._device_name = self._unit.name if self._unit.name else self._device_model
|
||||
|
||||
self._attr_unique_id = f"{self.unit.serial_number}{self.entity_description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
name=self._device_name,
|
||||
identifiers={(DOMAIN, str(self._unit.serial_number))},
|
||||
manufacturer="Aseko",
|
||||
model=self._device_model,
|
||||
identifiers={(DOMAIN, self.unit.serial_number)},
|
||||
serial_number=self.unit.serial_number,
|
||||
name=unit.name or unit.serial_number,
|
||||
manufacturer=(
|
||||
self.unit.brand_name.primary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
model=(
|
||||
self.unit.brand_name.secondary
|
||||
if self.unit.brand_name is not None
|
||||
else None
|
||||
),
|
||||
configuration_url=f"https://aseko.cloud/unit/{self.unit.serial_number}",
|
||||
)
|
||||
|
||||
@property
|
||||
def unit(self) -> Unit:
|
||||
"""Return the aseko unit."""
|
||||
return self.coordinator.data[self._unit.serial_number]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self.unit.serial_number in self.coordinator.data
|
||||
and self.unit.online
|
||||
)
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow": {
|
||||
"water_flow_to_probes": {
|
||||
"default": "mdi:waves-arrow-right"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"air_temperature": {
|
||||
"default": "mdi:thermometer-lines"
|
||||
},
|
||||
"free_chlorine": {
|
||||
"default": "mdi:flask"
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"redox": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"salinity": {
|
||||
"default": "mdi:pool"
|
||||
},
|
||||
"water_temperature": {
|
||||
"default": "mdi:coolant-temperature"
|
||||
"default": "mdi:pool-thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaseko"],
|
||||
"requirements": ["aioaseko==0.2.0"]
|
||||
"requirements": ["aioaseko==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -2,77 +2,104 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aioaseko import Unit, Variable
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aioaseko import Unit
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfElectricPotential, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AsekoDataUpdateCoordinator
|
||||
from .entity import AsekoEntity
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AsekoSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Aseko sensor entity."""
|
||||
|
||||
value_fn: Callable[[Unit], StateType]
|
||||
|
||||
|
||||
SENSORS: list[AsekoSensorEntityDescription] = [
|
||||
AsekoSensorEntityDescription(
|
||||
key="airTemp",
|
||||
translation_key="air_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.air_temperature,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="free_chlorine",
|
||||
translation_key="free_chlorine",
|
||||
native_unit_of_measurement="mg/l",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.cl_free,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="ph",
|
||||
device_class=SensorDeviceClass.PH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.ph,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="rx",
|
||||
translation_key="redox",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.redox,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="salinity",
|
||||
translation_key="salinity",
|
||||
native_unit_of_measurement="kg/m³",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.salinity,
|
||||
),
|
||||
AsekoSensorEntityDescription(
|
||||
key="waterTemp",
|
||||
translation_key="water_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda unit: unit.water_temperature,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Aseko Pool Live sensors."""
|
||||
data: list[tuple[Unit, AsekoDataUpdateCoordinator]] = hass.data[DOMAIN][
|
||||
config_entry.entry_id
|
||||
]
|
||||
|
||||
coordinator: AsekoDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
units = coordinator.data.values()
|
||||
async_add_entities(
|
||||
VariableSensorEntity(unit, variable, coordinator)
|
||||
for unit, coordinator in data
|
||||
for variable in unit.variables
|
||||
AsekoSensorEntity(unit, coordinator, description)
|
||||
for description in SENSORS
|
||||
for unit in units
|
||||
if description.value_fn(unit) is not None
|
||||
)
|
||||
|
||||
|
||||
class VariableSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of a unit variable sensor entity."""
|
||||
class AsekoSensorEntity(AsekoEntity, SensorEntity):
|
||||
"""Representation of an Aseko unit sensor entity."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
|
||||
def __init__(
|
||||
self, unit: Unit, variable: Variable, coordinator: AsekoDataUpdateCoordinator
|
||||
) -> None:
|
||||
"""Initialize the variable sensor."""
|
||||
super().__init__(unit, coordinator)
|
||||
self._variable = variable
|
||||
|
||||
translation_key = {
|
||||
"Air temp.": "air_temperature",
|
||||
"Cl free": "free_chlorine",
|
||||
"Water temp.": "water_temperature",
|
||||
}.get(self._variable.name)
|
||||
if translation_key is not None:
|
||||
self._attr_translation_key = translation_key
|
||||
else:
|
||||
self._attr_name = self._variable.name
|
||||
|
||||
self._attr_unique_id = f"{self._unit.serial_number}{self._variable.type}"
|
||||
self._attr_native_unit_of_measurement = self._variable.unit
|
||||
|
||||
self._attr_icon = {
|
||||
"rx": "mdi:test-tube",
|
||||
"waterLevel": "mdi:waves",
|
||||
}.get(self._variable.type)
|
||||
|
||||
self._attr_device_class = {
|
||||
"airTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"waterTemp": SensorDeviceClass.TEMPERATURE,
|
||||
"ph": SensorDeviceClass.PH,
|
||||
}.get(self._variable.type)
|
||||
entity_description: AsekoSensorEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
variable = self.coordinator.data[self._variable.type]
|
||||
return variable.current_value
|
||||
return self.entity_description.value_fn(self.unit)
|
||||
|
||||
@@ -26,11 +26,8 @@
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"water_flow": {
|
||||
"name": "Water flow"
|
||||
},
|
||||
"alarm": {
|
||||
"name": "Alarm"
|
||||
"water_flow_to_probes": {
|
||||
"name": "Water flow to probes"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
@@ -40,6 +37,12 @@
|
||||
"free_chlorine": {
|
||||
"name": "Free chlorine"
|
||||
},
|
||||
"redox": {
|
||||
"name": "Redox potential"
|
||||
},
|
||||
"salinity": {
|
||||
"name": "Salinity"
|
||||
},
|
||||
"water_temperature": {
|
||||
"name": "Water temperature"
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==8.6.2", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==8.6.4", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ from homeassistant.const import CONF_HOST, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import DOMAIN
|
||||
from .websocket import BangOlufsenWebsocket
|
||||
@@ -48,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
model=entry.data[CONF_MODEL],
|
||||
)
|
||||
|
||||
client = MozartClient(host=entry.data[CONF_HOST])
|
||||
client = MozartClient(host=entry.data[CONF_HOST], ssl_context=get_default_context())
|
||||
|
||||
# Check API and WebSocket connection
|
||||
try:
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_MODEL
|
||||
from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
|
||||
from homeassistant.util.ssl import get_default_context
|
||||
|
||||
from .const import (
|
||||
ATTR_FRIENDLY_NAME,
|
||||
@@ -87,7 +88,9 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors={"base": _exception_map[type(error)]},
|
||||
)
|
||||
|
||||
self._client = MozartClient(self._host)
|
||||
self._client = MozartClient(
|
||||
host=self._host, ssl_context=get_default_context()
|
||||
)
|
||||
|
||||
# Try to get information from Beolink self method.
|
||||
async with self._client:
|
||||
@@ -136,7 +139,7 @@ class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="ipv6_address")
|
||||
|
||||
# Check connection to ensure valid address is received
|
||||
self._client = MozartClient(self._host)
|
||||
self._client = MozartClient(self._host, ssl_context=get_default_context())
|
||||
|
||||
async with self._client:
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["mozart-api==3.4.1.8.6"],
|
||||
"requirements": ["mozart-api==3.4.1.8.8"],
|
||||
"zeroconf": ["_bangolufsen._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ COLOR_MODE_MAP = {
|
||||
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
"""Representation of BleBox lights."""
|
||||
|
||||
_attr_max_mireds = 370 # 1,000,000 divided by 2700 Kelvin = 370 Mireds
|
||||
_attr_min_mireds = 154 # 1,000,000 divided by 6500 Kelvin = 154 Mireds
|
||||
|
||||
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
|
||||
"""Initialize a BleBox light."""
|
||||
super().__init__(feature)
|
||||
@@ -87,12 +90,7 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
|
||||
|
||||
Set values to _attr_ibutes if needed.
|
||||
"""
|
||||
color_mode_tmp = COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
||||
if color_mode_tmp == ColorMode.COLOR_TEMP:
|
||||
self._attr_min_mireds = 1
|
||||
self._attr_max_mireds = 255
|
||||
|
||||
return color_mode_tmp
|
||||
return COLOR_MODE_MAP.get(self._feature.color_mode, ColorMode.ONOFF)
|
||||
|
||||
@property
|
||||
def supported_color_modes(self):
|
||||
|
||||
@@ -20,5 +20,8 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"info": data.info.to_dict(),
|
||||
"device": data.device.to_dict(),
|
||||
"state": data.coordinator.data.state.to_dict(),
|
||||
"coordinator_data": {
|
||||
"state": data.coordinator.data.state.to_dict(),
|
||||
},
|
||||
"static": data.static.to_dict(),
|
||||
}
|
||||
|
||||
@@ -22,10 +22,10 @@ class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]):
|
||||
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
|
||||
"""Initialize BSBLan entity."""
|
||||
super().__init__(coordinator, data)
|
||||
host = self.coordinator.config_entry.data["host"]
|
||||
mac = self.coordinator.config_entry.data["mac"]
|
||||
host = coordinator.config_entry.data["host"]
|
||||
mac = data.device.MAC
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, data.device.MAC)},
|
||||
identifiers={(DOMAIN, mac)},
|
||||
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
|
||||
name=data.device.name,
|
||||
manufacturer="BSBLAN Inc.",
|
||||
|
||||
@@ -7,6 +7,9 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA
|
||||
from homeassistant.components.device_automation.exceptions import (
|
||||
InvalidDeviceAutomationConfig,
|
||||
)
|
||||
from homeassistant.components.homeassistant.triggers import event as event_trigger
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
@@ -43,33 +46,46 @@ TRIGGERS_BY_EVENT_CLASS = {
|
||||
EVENT_CLASS_DIMMER: {"rotate_left", "rotate_right"},
|
||||
}
|
||||
|
||||
SCHEMA_BY_EVENT_CLASS = {
|
||||
EVENT_CLASS_BUTTON: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_BUTTON]),
|
||||
vol.Required(CONF_SUBTYPE): vol.In(
|
||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_BUTTON]
|
||||
),
|
||||
}
|
||||
),
|
||||
EVENT_CLASS_DIMMER: DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_TYPE): vol.In([EVENT_CLASS_DIMMER]),
|
||||
vol.Required(CONF_SUBTYPE): vol.In(
|
||||
TRIGGERS_BY_EVENT_CLASS[EVENT_CLASS_DIMMER]
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend(
|
||||
{vol.Required(CONF_TYPE): str, vol.Required(CONF_SUBTYPE): str}
|
||||
)
|
||||
|
||||
|
||||
async def async_validate_trigger_config(
|
||||
hass: HomeAssistant, config: ConfigType
|
||||
) -> ConfigType:
|
||||
"""Validate trigger config."""
|
||||
return SCHEMA_BY_EVENT_CLASS.get(config[CONF_TYPE], DEVICE_TRIGGER_BASE_SCHEMA)( # type: ignore[no-any-return]
|
||||
config
|
||||
config = TRIGGER_SCHEMA(config)
|
||||
event_class = config[CONF_TYPE]
|
||||
event_type = config[CONF_SUBTYPE]
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device = device_registry.async_get(config[CONF_DEVICE_ID])
|
||||
assert device is not None
|
||||
config_entries = [
|
||||
hass.config_entries.async_get_entry(entry_id)
|
||||
for entry_id in device.config_entries
|
||||
]
|
||||
bthome_config_entry = next(
|
||||
iter(entry for entry in config_entries if entry and entry.domain == DOMAIN)
|
||||
)
|
||||
event_classes: list[str] = bthome_config_entry.data.get(
|
||||
CONF_DISCOVERED_EVENT_CLASSES, []
|
||||
)
|
||||
|
||||
if event_class not in event_classes:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_class} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
)
|
||||
|
||||
if event_type not in TRIGGERS_BY_EVENT_CLASS.get(event_class.split("_")[0], ()):
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"BTHome trigger {event_type} is not valid for device "
|
||||
f"{device} ({config[CONF_DEVICE_ID]})"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
async def async_get_triggers(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.29"]
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.9.4"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.13.6"],
|
||||
"requirements": ["pydaikin==2.13.7"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==8.4.0"]
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ class ElevenLabsTTSEntity(TextToSpeechEntity):
|
||||
"""Load tts audio file from the engine."""
|
||||
_LOGGER.debug("Getting TTS audio for %s", message)
|
||||
_LOGGER.debug("Options: %s", options)
|
||||
voice_id = options[ATTR_VOICE]
|
||||
voice_id = options.get(ATTR_VOICE, self._default_voice_id)
|
||||
try:
|
||||
audio = await self._client.generate(
|
||||
text=message,
|
||||
|
||||
@@ -119,7 +119,7 @@ class EnvisalinkAlarm(EnvisalinkDevice, AlarmControlPanelEntity):
|
||||
self._partition_number = partition_number
|
||||
self._panic_type = panic_type
|
||||
self._alarm_control_panel_option_default_code = code
|
||||
self._attr_code_format = CodeFormat.NUMBER
|
||||
self._attr_code_format = CodeFormat.NUMBER if not code else None
|
||||
|
||||
_LOGGER.debug("Setting up alarm: %s", alarm_name)
|
||||
super().__init__(alarm_name, info, controller)
|
||||
|
||||
@@ -83,20 +83,38 @@ def entity_category_temperature(device: FritzhomeDevice) -> EntityCategory | Non
|
||||
return None
|
||||
|
||||
|
||||
def value_nextchange_preset(device: FritzhomeDevice) -> str:
|
||||
def value_nextchange_preset(device: FritzhomeDevice) -> str | None:
|
||||
"""Return native value for next scheduled preset sensor."""
|
||||
if not device.nextchange_endperiod:
|
||||
return None
|
||||
if device.nextchange_temperature == device.eco_temperature:
|
||||
return PRESET_ECO
|
||||
return PRESET_COMFORT
|
||||
|
||||
|
||||
def value_scheduled_preset(device: FritzhomeDevice) -> str:
|
||||
def value_scheduled_preset(device: FritzhomeDevice) -> str | None:
|
||||
"""Return native value for current scheduled preset sensor."""
|
||||
if not device.nextchange_endperiod:
|
||||
return None
|
||||
if device.nextchange_temperature == device.eco_temperature:
|
||||
return PRESET_COMFORT
|
||||
return PRESET_ECO
|
||||
|
||||
|
||||
def value_nextchange_temperature(device: FritzhomeDevice) -> float | None:
|
||||
"""Return native value for next scheduled temperature time sensor."""
|
||||
if device.nextchange_endperiod and isinstance(device.nextchange_temperature, float):
|
||||
return device.nextchange_temperature
|
||||
return None
|
||||
|
||||
|
||||
def value_nextchange_time(device: FritzhomeDevice) -> datetime | None:
|
||||
"""Return native value for next scheduled changed time sensor."""
|
||||
if device.nextchange_endperiod:
|
||||
return utc_from_timestamp(device.nextchange_endperiod)
|
||||
return None
|
||||
|
||||
|
||||
SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
FritzSensorEntityDescription(
|
||||
key="temperature",
|
||||
@@ -181,7 +199,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=suitable_nextchange_temperature,
|
||||
native_value=lambda device: device.nextchange_temperature,
|
||||
native_value=value_nextchange_temperature,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="nextchange_time",
|
||||
@@ -189,7 +207,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=suitable_nextchange_time,
|
||||
native_value=lambda device: utc_from_timestamp(device.nextchange_endperiod),
|
||||
native_value=value_nextchange_time,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="nextchange_preset",
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240903.1"]
|
||||
"requirements": ["home-assistant-frontend==20240909.1"]
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class FytaCoordinator(DataUpdateCoordinator[dict[int, Plant]]):
|
||||
hass,
|
||||
_LOGGER,
|
||||
name="FYTA Coordinator",
|
||||
update_interval=timedelta(seconds=60),
|
||||
update_interval=timedelta(minutes=4),
|
||||
)
|
||||
self.fyta = fyta
|
||||
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/gardena_bluetooth",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bleak", "bleak_esphome", "gardena_bluetooth"],
|
||||
"requirements": ["gardena-bluetooth==1.4.2"]
|
||||
"requirements": ["gardena-bluetooth==1.4.3"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==1.5.1"]
|
||||
"requirements": ["govee-local-api==1.5.2"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
@@ -14,25 +14,44 @@ from homeassistant.util import dt as dt_util
|
||||
def next_due_date(task: dict[str, Any], last_cron: str) -> datetime.date | None:
|
||||
"""Calculate due date for dailies and yesterdailies."""
|
||||
|
||||
today = to_date(last_cron)
|
||||
startdate = to_date(task["startDate"])
|
||||
if TYPE_CHECKING:
|
||||
assert today
|
||||
assert startdate
|
||||
|
||||
if task["isDue"] and not task["completed"]:
|
||||
return dt_util.as_local(datetime.datetime.fromisoformat(last_cron)).date()
|
||||
return to_date(last_cron)
|
||||
|
||||
if startdate > today:
|
||||
if task["frequency"] == "daily" or (
|
||||
task["frequency"] in ("monthly", "yearly") and task["daysOfMonth"]
|
||||
):
|
||||
return startdate
|
||||
|
||||
if (
|
||||
task["frequency"] in ("weekly", "monthly")
|
||||
and (nextdue := to_date(task["nextDue"][0]))
|
||||
and startdate > nextdue
|
||||
):
|
||||
return to_date(task["nextDue"][1])
|
||||
|
||||
return to_date(task["nextDue"][0])
|
||||
|
||||
|
||||
def to_date(date: str) -> datetime.date | None:
|
||||
"""Convert an iso date to a datetime.date object."""
|
||||
try:
|
||||
return dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task["nextDue"][0])
|
||||
).date()
|
||||
return dt_util.as_local(datetime.datetime.fromisoformat(date)).date()
|
||||
except ValueError:
|
||||
# sometimes nextDue dates are in this format instead of iso:
|
||||
# sometimes nextDue dates are JavaScript datetime strings instead of iso:
|
||||
# "Mon May 06 2024 00:00:00 GMT+0200"
|
||||
try:
|
||||
return dt_util.as_local(
|
||||
datetime.datetime.strptime(
|
||||
task["nextDue"][0], "%a %b %d %Y %H:%M:%S %Z%z"
|
||||
)
|
||||
datetime.datetime.strptime(date, "%a %b %d %Y %H:%M:%S %Z%z")
|
||||
).date()
|
||||
except ValueError:
|
||||
return None
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
|
||||
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.55", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.57", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
"not_hassio_thread": "The OpenThread Border Router addon can only be installed with Home Assistant OS. If you would like to use the {model} as an Thread border router, please flash the firmware manually using the [web flasher]({docs_web_flasher_url}) and set up OpenThread Border Router to communicate with it.",
|
||||
"otbr_addon_already_running": "The OpenThread Border Router add-on is already running, it cannot be installed again.",
|
||||
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again."
|
||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device. If you are running Home Assistant OS in a virtual machine or in Docker, please make sure that permissions are set correctly for the device."
|
||||
},
|
||||
"progress": {
|
||||
"install_zigbee_flasher_addon": "The Silicon Labs Flasher addon is installed, this may take a few minutes.",
|
||||
|
||||
@@ -113,7 +113,8 @@
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
@@ -181,7 +182,10 @@
|
||||
"zha_migration_failed": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::zha_migration_failed%]",
|
||||
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]"
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::unsupported_firmware%]"
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
|
||||
@@ -138,7 +138,8 @@
|
||||
"not_hassio_thread": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::not_hassio_thread%]",
|
||||
"otbr_addon_already_running": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_addon_already_running%]",
|
||||
"zha_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::zha_still_using_stick%]",
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]"
|
||||
"otbr_still_using_stick": "[%key:component::homeassistant_hardware::firmware_picker::options::abort::otbr_still_using_stick%]",
|
||||
"unsupported_firmware": "The radio firmware on your {model} could not be determined. Make sure that no other integration or addon is currently trying to communicate with the device."
|
||||
},
|
||||
"progress": {
|
||||
"install_addon": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::progress::install_addon%]",
|
||||
|
||||
@@ -226,7 +226,11 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str in (EFFECT_NONE, EFFECT_NONE.lower()):
|
||||
effect = EffectStatus.NO_EFFECT
|
||||
# ignore effect if set to "None" and we have no effect active
|
||||
# the special effect "None" is only used to stop an active effect
|
||||
# but sending it while no effect is active can actually result in issues
|
||||
# https://github.com/home-assistant/core/issues/122165
|
||||
effect = None if self.effect == EFFECT_NONE else EffectStatus.NO_EFFECT
|
||||
elif effect_str is not None:
|
||||
# work out if we got a regular effect or timed effect
|
||||
effect = EffectStatus(effect_str)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2024.8.0"]
|
||||
"requirements": ["pydrawise==2024.9.0"]
|
||||
}
|
||||
|
||||
@@ -90,8 +90,10 @@ class InComfortClimate(IncomfortEntity, ClimateEntity):
|
||||
|
||||
As we set the override, we report back the override. The actual set point is
|
||||
is returned at a later time.
|
||||
Some older thermostats return 0.0 as override, in that case we fallback to
|
||||
the actual setpoint.
|
||||
"""
|
||||
return self._room.override
|
||||
return self._room.override or self._room.setpoint
|
||||
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new target temperature for this zone."""
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from jvcprojector import (
|
||||
JvcProjector,
|
||||
@@ -40,7 +41,7 @@ class JvcProjectorDataUpdateCoordinator(DataUpdateCoordinator[dict[str, str]]):
|
||||
self.device = device
|
||||
self.unique_id = format_mac(device.mac)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, str]:
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Get the latest state data."""
|
||||
try:
|
||||
state = await self.device.get_state()
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==1.0.12"]
|
||||
"requirements": ["pyjvcprojector==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from xknx.devices import Device as XknxDevice
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry
|
||||
|
||||
from .const import DOMAIN
|
||||
from .storage.config_store import PlatformControllerBase
|
||||
from .storage.const import CONF_DEVICE_INFO
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import KNXModule
|
||||
|
||||
from .storage.config_store import PlatformControllerBase
|
||||
|
||||
|
||||
class KnxUiEntityPlatformController(PlatformControllerBase):
|
||||
"""Class to manage dynamic adding and reloading of UI entities."""
|
||||
@@ -93,13 +96,19 @@ class KnxYamlEntity(_KnxEntityBase):
|
||||
self._device = device
|
||||
|
||||
|
||||
class KnxUiEntity(_KnxEntityBase, ABC):
|
||||
class KnxUiEntity(_KnxEntityBase):
|
||||
"""Representation of a KNX UI entity."""
|
||||
|
||||
_attr_unique_id: str
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@abstractmethod
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
||||
self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize the UI entity."""
|
||||
self._knx_module = knx_module
|
||||
self._attr_unique_id = unique_id
|
||||
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
|
||||
self._attr_entity_category = EntityCategory(entity_category)
|
||||
if device_info := entity_config.get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
||||
@@ -20,7 +20,6 @@ from homeassistant.components.light import (
|
||||
)
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@@ -35,7 +34,6 @@ from .schema import LightSchema
|
||||
from .storage.const import (
|
||||
CONF_COLOR_TEMP_MAX,
|
||||
CONF_COLOR_TEMP_MIN,
|
||||
CONF_DEVICE_INFO,
|
||||
CONF_DPT,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_BLUE_BRIGHTNESS,
|
||||
@@ -554,21 +552,19 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity):
|
||||
class KnxUiLight(_KnxLight, KnxUiEntity):
|
||||
"""Representation of a KNX light."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_device: XknxLight
|
||||
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: ConfigType
|
||||
) -> None:
|
||||
"""Initialize of KNX light."""
|
||||
self._knx_module = knx_module
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
self._device = _create_ui_light(
|
||||
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
|
||||
)
|
||||
self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX]
|
||||
self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN]
|
||||
|
||||
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
|
||||
self._attr_unique_id = unique_id
|
||||
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.1.1",
|
||||
"xknxproject==3.7.1",
|
||||
"knx-frontend==2024.8.9.225351"
|
||||
"knx-frontend==2024.9.10.221729"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ def parse_invalid(exc: vol.Invalid) -> _ErrorDescription:
|
||||
|
||||
|
||||
def validate_entity_data(entity_data: dict) -> dict:
|
||||
"""Validate entity data. Return validated data or raise EntityStoreValidationException."""
|
||||
"""Validate entity data.
|
||||
|
||||
Return validated data or raise EntityStoreValidationException.
|
||||
"""
|
||||
try:
|
||||
# return so defaults are applied
|
||||
return ENTITY_STORE_DATA_SCHEMA(entity_data) # type: ignore[no-any-return]
|
||||
|
||||
@@ -18,7 +18,6 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddEntitiesCallback,
|
||||
async_get_current_platform,
|
||||
@@ -38,7 +37,6 @@ from .const import (
|
||||
from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
|
||||
from .schema import SwitchSchema
|
||||
from .storage.const import (
|
||||
CONF_DEVICE_INFO,
|
||||
CONF_ENTITY,
|
||||
CONF_GA_PASSIVE,
|
||||
CONF_GA_STATE,
|
||||
@@ -133,14 +131,17 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
|
||||
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
"""Representation of a KNX switch configured from UI."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_device: XknxSwitch
|
||||
|
||||
def __init__(
|
||||
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
|
||||
) -> None:
|
||||
"""Initialize KNX switch."""
|
||||
self._knx_module = knx_module
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=unique_id,
|
||||
entity_config=config[CONF_ENTITY],
|
||||
)
|
||||
self._device = XknxSwitch(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_ENTITY][CONF_NAME],
|
||||
@@ -153,7 +154,3 @@ class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
sync_state=config[DOMAIN][CONF_SYNC_STATE],
|
||||
invert=config[DOMAIN][CONF_INVERT],
|
||||
)
|
||||
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
|
||||
self._attr_unique_id = unique_id
|
||||
if device_info := config[CONF_ENTITY].get(CONF_DEVICE_INFO):
|
||||
self._attr_device_info = DeviceInfo(identifiers={(DOMAIN, device_info)})
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lmcloud"],
|
||||
"requirements": ["lmcloud==1.2.2"]
|
||||
"requirements": ["lmcloud==1.2.3"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.7.21", "lcn-frontend==0.1.6"]
|
||||
"requirements": ["pypck==0.7.22", "lcn-frontend==0.1.6"]
|
||||
}
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
|
||||
"requirements": [
|
||||
"aiolifx==1.0.9",
|
||||
"aiolifx==1.1.1",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.5.0"
|
||||
"aiolifx-themes==0.5.5"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ from homeassistant.components.media_player import (
|
||||
MediaType,
|
||||
RepeatMode,
|
||||
)
|
||||
from homeassistant.components.media_player.browse_media import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -59,6 +62,7 @@ SOURCE_MAP: dict[PlayingMode, str] = {
|
||||
PlayingMode.FM: "FM Radio",
|
||||
PlayingMode.RCA: "RCA",
|
||||
PlayingMode.UDISK: "USB",
|
||||
PlayingMode.FOLLOWER: "Follower",
|
||||
}
|
||||
|
||||
SOURCE_MAP_INV: dict[str, PlayingMode] = {v: k for k, v in SOURCE_MAP.items()}
|
||||
@@ -233,10 +237,14 @@ class LinkPlayMediaPlayerEntity(MediaPlayerEntity):
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
"""Play a piece of media."""
|
||||
media = await media_source.async_resolve_media(
|
||||
self.hass, media_id, self.entity_id
|
||||
)
|
||||
await self._bridge.player.play(media.url)
|
||||
if media_source.is_media_source_id(media_id):
|
||||
play_item = await media_source.async_resolve_media(
|
||||
self.hass, media_id, self.entity_id
|
||||
)
|
||||
media_id = play_item.url
|
||||
|
||||
url = async_process_play_media_url(self.hass, media_id)
|
||||
await self._bridge.player.play(url)
|
||||
|
||||
def _update_properties(self) -> None:
|
||||
"""Update the properties of the media player."""
|
||||
|
||||
@@ -208,10 +208,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
if LYRIC_HVAC_MODE_COOL in device.allowed_modes:
|
||||
self._attr_hvac_modes.append(HVACMode.COOL)
|
||||
|
||||
if (
|
||||
LYRIC_HVAC_MODE_HEAT in device.allowed_modes
|
||||
and LYRIC_HVAC_MODE_COOL in device.allowed_modes
|
||||
):
|
||||
if LYRIC_HVAC_MODE_HEAT_COOL in device.allowed_modes:
|
||||
self._attr_hvac_modes.append(HVACMode.HEAT_COOL)
|
||||
|
||||
# Setup supported features
|
||||
@@ -358,8 +355,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
device,
|
||||
coolSetpoint=target_temp_high,
|
||||
heatSetpoint=target_temp_low,
|
||||
cool_setpoint=target_temp_high,
|
||||
heat_setpoint=target_temp_low,
|
||||
mode=mode,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
@@ -371,11 +368,11 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
try:
|
||||
if self.hvac_mode == HVACMode.COOL:
|
||||
await self._update_thermostat(
|
||||
self.location, device, coolSetpoint=temp
|
||||
self.location, device, cool_setpoint=temp
|
||||
)
|
||||
else:
|
||||
await self._update_thermostat(
|
||||
self.location, device, heatSetpoint=temp
|
||||
self.location, device, heat_setpoint=temp
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
@@ -410,7 +407,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=False,
|
||||
auto_changeover_active=False,
|
||||
)
|
||||
# Sleep 3 seconds before proceeding
|
||||
await asyncio.sleep(3)
|
||||
@@ -422,7 +419,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=HVAC_MODES[LYRIC_HVAC_MODE_HEAT],
|
||||
autoChangeoverActive=True,
|
||||
auto_changeover_active=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -430,7 +427,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
HVAC_MODES[self.device.changeable_values.mode],
|
||||
)
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, autoChangeoverActive=True
|
||||
self.location, self.device, auto_changeover_active=True
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
@@ -438,13 +435,13 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=False,
|
||||
auto_changeover_active=False,
|
||||
)
|
||||
|
||||
async def _async_set_hvac_mode_lcc(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode for LCC devices (e.g., T5,6)."""
|
||||
_LOGGER.debug("HVAC mode passed to lyric: %s", LYRIC_HVAC_MODES[hvac_mode])
|
||||
# Set autoChangeoverActive to True if the mode being passed is Auto
|
||||
# Set auto_changeover_active to True if the mode being passed is Auto
|
||||
# otherwise leave unchanged.
|
||||
if (
|
||||
LYRIC_HVAC_MODES[hvac_mode] == LYRIC_HVAC_MODE_HEAT_COOL
|
||||
@@ -458,7 +455,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
self.location,
|
||||
self.device,
|
||||
mode=LYRIC_HVAC_MODES[hvac_mode],
|
||||
autoChangeoverActive=auto_changeover,
|
||||
auto_changeover_active=auto_changeover,
|
||||
)
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
@@ -466,7 +463,7 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
_LOGGER.debug("Set preset mode: %s", preset_mode)
|
||||
try:
|
||||
await self._update_thermostat(
|
||||
self.location, self.device, thermostatSetpointStatus=preset_mode
|
||||
self.location, self.device, thermostat_setpoint_status=preset_mode
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
@@ -479,8 +476,8 @@ class LyricClimate(LyricDeviceEntity, ClimateEntity):
|
||||
await self._update_thermostat(
|
||||
self.location,
|
||||
self.device,
|
||||
thermostatSetpointStatus=PRESET_HOLD_UNTIL,
|
||||
nextPeriodTime=time_period,
|
||||
thermostat_setpoint_status=PRESET_HOLD_UNTIL,
|
||||
next_period_time=time_period,
|
||||
)
|
||||
except LYRIC_EXCEPTIONS as exception:
|
||||
_LOGGER.error(exception)
|
||||
|
||||
@@ -190,48 +190,56 @@ class MatterClimate(MatterEntity, ClimateEntity):
|
||||
# if the mains power is off - treat it as if the HVAC mode is off
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
self._attr_hvac_action = None
|
||||
return
|
||||
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(clusters.Thermostat.Attributes.SystemMode)
|
||||
)
|
||||
match system_mode_value:
|
||||
case SystemModeEnum.kAuto:
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
|
||||
self._attr_hvac_mode = HVACMode.COOL
|
||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case _:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
# running state is an optional attribute
|
||||
# which we map to hvac_action if it exists (its value is not None)
|
||||
self._attr_hvac_action = None
|
||||
if running_state_value := self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState
|
||||
):
|
||||
match running_state_value:
|
||||
case ThermostatRunningState.Heat | ThermostatRunningState.HeatStage2:
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
case ThermostatRunningState.Cool | ThermostatRunningState.CoolStage2:
|
||||
self._attr_hvac_action = HVACAction.COOLING
|
||||
case (
|
||||
ThermostatRunningState.Fan
|
||||
| ThermostatRunningState.FanStage2
|
||||
| ThermostatRunningState.FanStage3
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
else:
|
||||
# update hvac_mode from SystemMode
|
||||
system_mode_value = int(
|
||||
self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.SystemMode
|
||||
)
|
||||
)
|
||||
match system_mode_value:
|
||||
case SystemModeEnum.kAuto:
|
||||
self._attr_hvac_mode = HVACMode.HEAT_COOL
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kCool | SystemModeEnum.kPrecooling:
|
||||
self._attr_hvac_mode = HVACMode.COOL
|
||||
case SystemModeEnum.kHeat | SystemModeEnum.kEmergencyHeat:
|
||||
self._attr_hvac_mode = HVACMode.HEAT
|
||||
case SystemModeEnum.kFanOnly:
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
case SystemModeEnum.kDry:
|
||||
self._attr_hvac_mode = HVACMode.DRY
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
# running state is an optional attribute
|
||||
# which we map to hvac_action if it exists (its value is not None)
|
||||
self._attr_hvac_action = None
|
||||
if running_state_value := self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.ThermostatRunningState
|
||||
):
|
||||
match running_state_value:
|
||||
case (
|
||||
ThermostatRunningState.Heat
|
||||
| ThermostatRunningState.HeatStage2
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.HEATING
|
||||
case (
|
||||
ThermostatRunningState.Cool
|
||||
| ThermostatRunningState.CoolStage2
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.COOLING
|
||||
case (
|
||||
ThermostatRunningState.Fan
|
||||
| ThermostatRunningState.FanStage2
|
||||
| ThermostatRunningState.FanStage3
|
||||
):
|
||||
self._attr_hvac_action = HVACAction.FAN
|
||||
case _:
|
||||
self._attr_hvac_action = HVACAction.OFF
|
||||
|
||||
# update target temperature high/low
|
||||
supports_range = (
|
||||
self._attr_supported_features
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from aiomealie import MealieAuthenticationError, MealieClient, MealieConnectionError
|
||||
from aiomealie import MealieAuthenticationError, MealieClient, MealieError
|
||||
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -53,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: MealieConfigEntry) -> bo
|
||||
version = create_version(about.version)
|
||||
except MealieAuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
except MealieConnectionError as error:
|
||||
except MealieError as error:
|
||||
raise ConfigEntryNotReady(error) from error
|
||||
|
||||
if not version.valid:
|
||||
|
||||
@@ -26,7 +26,13 @@ async def async_setup_entry(
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
|
||||
|
||||
for blind in motion_gateway.device_list.values():
|
||||
if blind.limit_status == LimitStatus.Limit3Detected.name:
|
||||
if blind.limit_status in (
|
||||
LimitStatus.Limit3Detected.name,
|
||||
{
|
||||
"T": LimitStatus.Limit3Detected.name,
|
||||
"B": LimitStatus.Limit3Detected.name,
|
||||
},
|
||||
):
|
||||
entities.append(MotionGoFavoriteButton(coordinator, blind))
|
||||
entities.append(MotionSetFavoriteButton(coordinator, blind))
|
||||
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/motion_blinds",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["motionblinds"],
|
||||
"requirements": ["motionblinds==0.6.24"]
|
||||
"requirements": ["motionblinds==0.6.25"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==5.0.0"]
|
||||
"requirements": ["google-nest-sdm==5.0.1"]
|
||||
}
|
||||
|
||||
@@ -173,7 +173,9 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn light on."""
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
await self.device.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
|
||||
await self.device.async_set_brightness(
|
||||
round(kwargs[ATTR_BRIGHTNESS] / 2.55)
|
||||
)
|
||||
|
||||
else:
|
||||
await self.device.async_on()
|
||||
@@ -194,6 +196,6 @@ class NetatmoLight(NetatmoModuleEntity, LightEntity):
|
||||
|
||||
if (brightness := self.device.brightness) is not None:
|
||||
# Netatmo uses a range of [0, 100] to control brightness
|
||||
self._attr_brightness = round((brightness / 100) * 255)
|
||||
self._attr_brightness = round(brightness * 2.55)
|
||||
else:
|
||||
self._attr_brightness = None
|
||||
|
||||
@@ -96,11 +96,10 @@ class PS4Device(MediaPlayerEntity):
|
||||
self._retry = 0
|
||||
self._disconnected = False
|
||||
|
||||
@callback
|
||||
def status_callback(self) -> None:
|
||||
"""Handle status callback. Parse status."""
|
||||
self._parse_status()
|
||||
self.async_write_ha_state()
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
def subscribe_to_protocol(self) -> None:
|
||||
@@ -157,7 +156,7 @@ class PS4Device(MediaPlayerEntity):
|
||||
self._ps4.ddp_protocol = self.hass.data[PS4_DATA].protocol
|
||||
self.subscribe_to_protocol()
|
||||
|
||||
self._parse_status()
|
||||
await self.hass.async_add_executor_job(self._parse_status)
|
||||
|
||||
def _parse_status(self) -> None:
|
||||
"""Parse status."""
|
||||
|
||||
@@ -177,8 +177,12 @@ def count_torrents_in_states(
|
||||
# When torrents are not in the returned data, there are none, return 0.
|
||||
try:
|
||||
torrents = cast(Mapping[str, Mapping], coordinator.data.get("torrents"))
|
||||
if torrents is None:
|
||||
return 0
|
||||
|
||||
if not states:
|
||||
return len(torrents)
|
||||
|
||||
return len(
|
||||
[torrent for torrent in torrents.values() if torrent.get("state") in states]
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ class RenaultBinarySensorEntityDescription(
|
||||
"""Class describing Renault binary sensor entities."""
|
||||
|
||||
on_key: str
|
||||
on_value: StateType
|
||||
on_value: StateType | list[StateType]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -58,6 +58,9 @@ class RenaultBinarySensor(
|
||||
"""Return true if the binary sensor is on."""
|
||||
if (data := self._get_data_attr(self.entity_description.on_key)) is None:
|
||||
return None
|
||||
|
||||
if isinstance(self.entity_description.on_value, list):
|
||||
return data in self.entity_description.on_value
|
||||
return data == self.entity_description.on_value
|
||||
|
||||
|
||||
@@ -68,7 +71,10 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
coordinator="battery",
|
||||
device_class=BinarySensorDeviceClass.PLUG,
|
||||
on_key="plugStatus",
|
||||
on_value=PlugState.PLUGGED.value,
|
||||
on_value=[
|
||||
PlugState.PLUGGED.value,
|
||||
PlugState.PLUGGED_WAITING_FOR_CHARGE.value,
|
||||
],
|
||||
),
|
||||
RenaultBinarySensorEntityDescription(
|
||||
key="charging",
|
||||
@@ -104,13 +110,13 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
|
||||
]
|
||||
+ [
|
||||
RenaultBinarySensorEntityDescription(
|
||||
key=f"{door.replace(' ','_').lower()}_door_status",
|
||||
key=f"{door.replace(' ', '_').lower()}_door_status",
|
||||
coordinator="lock_status",
|
||||
# On means open, Off means closed
|
||||
device_class=BinarySensorDeviceClass.DOOR,
|
||||
on_key=f"doorStatus{door.replace(' ','')}",
|
||||
on_key=f"doorStatus{door.replace(' ', '')}",
|
||||
on_value="open",
|
||||
translation_key=f"{door.lower().replace(' ','_')}_door_status",
|
||||
translation_key=f"{door.lower().replace(' ', '_')}_door_status",
|
||||
)
|
||||
for door in ("Rear Left", "Rear Right", "Driver", "Passenger")
|
||||
],
|
||||
|
||||
@@ -197,7 +197,13 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
|
||||
translation_key="plug_state",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
entity_class=RenaultSensor[KamereonVehicleBatteryStatusData],
|
||||
options=["unplugged", "plugged", "plug_error", "plug_unknown"],
|
||||
options=[
|
||||
"unplugged",
|
||||
"plugged",
|
||||
"plugged_waiting_for_charge",
|
||||
"plug_error",
|
||||
"plug_unknown",
|
||||
],
|
||||
value_lambda=_get_plug_state_formatted,
|
||||
),
|
||||
RenaultSensorEntityDescription(
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
"state": {
|
||||
"unplugged": "Unplugged",
|
||||
"plugged": "Plugged in",
|
||||
"plugged_waiting_for_charge": "Plugged in, waiting for charge",
|
||||
"plug_error": "Plug error",
|
||||
"plug_unknown": "Plug unknown"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from aiorussound import Russound
|
||||
from aiorussound import RussoundClient, RussoundTcpConnectionHandler
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
|
||||
@@ -16,7 +16,7 @@ PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type RussoundConfigEntry = ConfigEntry[Russound]
|
||||
type RussoundConfigEntry = ConfigEntry[RussoundClient]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool:
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
|
||||
|
||||
host = entry.data[CONF_HOST]
|
||||
port = entry.data[CONF_PORT]
|
||||
russ = Russound(hass.loop, host, port)
|
||||
russ = RussoundClient(RussoundTcpConnectionHandler(hass.loop, host, port))
|
||||
|
||||
@callback
|
||||
def is_connected_updated(connected: bool) -> None:
|
||||
@@ -37,14 +37,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
|
||||
port,
|
||||
)
|
||||
|
||||
russ.add_connection_callback(is_connected_updated)
|
||||
|
||||
russ.connection_handler.add_connection_callback(is_connected_updated)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
except RUSSOUND_RIO_EXCEPTIONS as err:
|
||||
raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err
|
||||
|
||||
entry.runtime_data = russ
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiorussound import Controller, Russound
|
||||
from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -54,8 +54,9 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
host = user_input[CONF_HOST]
|
||||
port = user_input[CONF_PORT]
|
||||
|
||||
controllers = None
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
russ = RussoundClient(
|
||||
RussoundTcpConnectionHandler(self.hass.loop, host, port)
|
||||
)
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
@@ -87,7 +88,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
port = import_data.get(CONF_PORT, 9621)
|
||||
|
||||
# Connection logic is repeated here since this method will be removed in future releases
|
||||
russ = Russound(self.hass.loop, host, port)
|
||||
russ = RussoundClient(RussoundTcpConnectionHandler(self.hass.loop, host, port))
|
||||
try:
|
||||
async with asyncio.timeout(CONNECT_TIMEOUT):
|
||||
await russ.connect()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
|
||||
from aiorussound import CommandException
|
||||
from aiorussound import CommandError
|
||||
from aiorussound.const import FeatureFlag
|
||||
|
||||
from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
@@ -10,7 +10,7 @@ from homeassistant.components.media_player import MediaPlayerEntityFeature
|
||||
DOMAIN = "russound_rio"
|
||||
|
||||
RUSSOUND_RIO_EXCEPTIONS = (
|
||||
CommandException,
|
||||
CommandError,
|
||||
ConnectionRefusedError,
|
||||
TimeoutError,
|
||||
asyncio.CancelledError,
|
||||
|
||||
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiorussound import Controller
|
||||
from aiorussound import Controller, RussoundTcpConnectionHandler
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
@@ -53,7 +53,6 @@ class RussoundBaseEntity(Entity):
|
||||
or f"{self._primary_mac_address}-{self._controller.controller_id}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"http://{self._instance.host}",
|
||||
# Use MAC address of Russound device as identifier
|
||||
identifiers={(DOMAIN, self._device_identifier)},
|
||||
manufacturer="Russound",
|
||||
@@ -61,6 +60,10 @@ class RussoundBaseEntity(Entity):
|
||||
model=controller.controller_type,
|
||||
sw_version=controller.firmware_version,
|
||||
)
|
||||
if isinstance(self._instance.connection_handler, RussoundTcpConnectionHandler):
|
||||
self._attr_device_info["configuration_url"] = (
|
||||
f"http://{self._instance.connection_handler.host}"
|
||||
)
|
||||
if controller.parent_controller:
|
||||
self._attr_device_info["via_device"] = (
|
||||
DOMAIN,
|
||||
@@ -79,8 +82,12 @@ class RussoundBaseEntity(Entity):
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callbacks."""
|
||||
self._instance.add_connection_callback(self._is_connected_updated)
|
||||
self._instance.connection_handler.add_connection_callback(
|
||||
self._is_connected_updated
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
self._instance.remove_connection_callback(self._is_connected_updated)
|
||||
self._instance.connection_handler.remove_connection_callback(
|
||||
self._is_connected_updated
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aiorussound==2.3.2"]
|
||||
"requirements": ["aiorussound==3.0.5"]
|
||||
}
|
||||
|
||||
@@ -84,14 +84,16 @@ async def async_setup_entry(
|
||||
"""Set up the Russound RIO platform."""
|
||||
russ = entry.runtime_data
|
||||
|
||||
await russ.init_sources()
|
||||
sources = russ.sources
|
||||
for source in sources.values():
|
||||
await source.watch()
|
||||
|
||||
# Discover controllers
|
||||
controllers = await russ.enumerate_controllers()
|
||||
|
||||
entities = []
|
||||
for controller in controllers.values():
|
||||
sources = controller.sources
|
||||
for source in sources.values():
|
||||
await source.watch()
|
||||
for zone in controller.zones.values():
|
||||
await zone.watch()
|
||||
mp = RussoundZoneDevice(zone, sources)
|
||||
@@ -154,7 +156,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the device."""
|
||||
status = self._zone.status
|
||||
status = self._zone.properties.status
|
||||
if status == "ON":
|
||||
return MediaPlayerState.ON
|
||||
if status == "OFF":
|
||||
@@ -174,22 +176,22 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
return self._current_source().song_name
|
||||
return self._current_source().properties.song_name
|
||||
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media, music track only."""
|
||||
return self._current_source().artist_name
|
||||
return self._current_source().properties.artist_name
|
||||
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self._current_source().album_name
|
||||
return self._current_source().properties.album_name
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
return self._current_source().cover_art_url
|
||||
return self._current_source().properties.cover_art_url
|
||||
|
||||
@property
|
||||
def volume_level(self):
|
||||
@@ -198,7 +200,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
Value is returned based on a range (0..50).
|
||||
Therefore float divide by 50 to get to the required range.
|
||||
"""
|
||||
return float(self._zone.volume or "0") / 50.0
|
||||
return float(self._zone.properties.volume or "0") / 50.0
|
||||
|
||||
@command
|
||||
async def async_turn_off(self) -> None:
|
||||
@@ -214,7 +216,7 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set the volume level."""
|
||||
rvol = int(volume * 50.0)
|
||||
await self._zone.set_volume(rvol)
|
||||
await self._zone.set_volume(str(rvol))
|
||||
|
||||
@command
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rnet",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["russound"],
|
||||
"requirements": ["russound==0.1.9"]
|
||||
"requirements": ["russound==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -96,7 +96,13 @@ class RussoundRNETDevice(MediaPlayerEntity):
|
||||
# Updated this function to make a single call to get_zone_info, so that
|
||||
# with a single call we can get On/Off, Volume and Source, reducing the
|
||||
# amount of traffic and speeding up the update process.
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
try:
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
except BrokenPipeError:
|
||||
_LOGGER.error("Broken Pipe Error, trying to reconnect to Russound RNET")
|
||||
self._russ.connect()
|
||||
ret = self._russ.get_zone_info(self._controller_id, self._zone_id, 4)
|
||||
|
||||
_LOGGER.debug("ret= %s", ret)
|
||||
if ret is not None:
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -42,5 +42,4 @@ class SchlageEntity(CoordinatorEntity[SchlageDataUpdateCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
# When is_locked is None the lock is unavailable.
|
||||
return super().available and self._lock.is_locked is not None
|
||||
return super().available and self.device_id in self.coordinator.data.locks
|
||||
|
||||
@@ -42,8 +42,9 @@ class SchlageLockEntity(SchlageEntity, LockEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._update_attrs()
|
||||
return super()._handle_coordinator_update()
|
||||
if self.device_id in self.coordinator.data.locks:
|
||||
self._update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
def _update_attrs(self) -> None:
|
||||
"""Update our internal state attributes."""
|
||||
|
||||
@@ -64,5 +64,6 @@ class SchlageBatterySensor(SchlageEntity, SensorEntity):
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_native_value = getattr(self._lock, self.entity_description.key)
|
||||
return super()._handle_coordinator_update()
|
||||
if self.device_id in self.coordinator.data.locks:
|
||||
self._attr_native_value = getattr(self._lock, self.entity_description.key)
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
|
||||
@@ -46,6 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Preload system information
|
||||
await data.system.async_config_entry_first_refresh()
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
# Preload other coordinators (based on net infrastructure)
|
||||
tasks = [data.wan.async_config_entry_first_refresh()]
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.models import DslInfo, FtthInfo, SystemInfo, WanInfo
|
||||
|
||||
@@ -65,19 +66,22 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities: list[SFRBoxBinarySensor] = [
|
||||
SFRBoxBinarySensor(data.wan, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.wan, description, system_info)
|
||||
for description in WAN_SENSOR_TYPES
|
||||
]
|
||||
if (net_infra := data.system.data.net_infra) == "adsl":
|
||||
if (net_infra := system_info.net_infra) == "adsl":
|
||||
entities.extend(
|
||||
SFRBoxBinarySensor(data.dsl, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.dsl, description, system_info)
|
||||
for description in DSL_SENSOR_TYPES
|
||||
)
|
||||
elif net_infra == "ftth":
|
||||
entities.extend(
|
||||
SFRBoxBinarySensor(data.ftth, description, data.system.data)
|
||||
SFRBoxBinarySensor(data.ftth, description, system_info)
|
||||
for description in FTTH_SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -111,4 +115,6 @@ class SFRBoxBinarySensor[_T](
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxError
|
||||
@@ -69,10 +69,12 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the buttons."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities = [
|
||||
SFRBoxButton(data.box, description, data.system.data)
|
||||
for description in BUTTON_TYPES
|
||||
SFRBoxButton(data.box, description, system_info) for description in BUTTON_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from sfrbox_api.bridge import SFRBox
|
||||
from sfrbox_api.exceptions import SFRBoxAuthenticationError, SFRBoxError
|
||||
@@ -51,6 +51,8 @@ class SFRBoxFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except SFRBoxError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
await self.async_set_unique_id(system_info.mac_addr)
|
||||
self._abort_if_unique_id_configured()
|
||||
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
|
||||
|
||||
@@ -15,7 +15,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
|
||||
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT | None]):
|
||||
"""Coordinator to manage data updates."""
|
||||
|
||||
def __init__(
|
||||
@@ -23,14 +23,14 @@ class SFRDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
hass: HomeAssistant,
|
||||
box: SFRBox,
|
||||
name: str,
|
||||
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT]],
|
||||
method: Callable[[SFRBox], Coroutine[Any, Any, _DataT | None]],
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
self.box = box
|
||||
self._method = method
|
||||
super().__init__(hass, _LOGGER, name=name, update_interval=_SCAN_INTERVAL)
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
async def _async_update_data(self) -> _DataT | None:
|
||||
"""Update data."""
|
||||
try:
|
||||
return await self._method(self.box)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -12,9 +12,18 @@ from homeassistant.core import HomeAssistant
|
||||
from .const import DOMAIN
|
||||
from .models import DomainData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from _typeshed import DataclassInstance
|
||||
|
||||
TO_REDACT = {"mac_addr", "serial_number", "ip_addr", "ipv6_addr"}
|
||||
|
||||
|
||||
def _async_redact_data(obj: DataclassInstance | None) -> dict[str, Any] | None:
|
||||
if obj is None:
|
||||
return None
|
||||
return async_redact_data(dataclasses.asdict(obj), TO_REDACT)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
@@ -27,21 +36,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"data": dict(entry.data),
|
||||
},
|
||||
"data": {
|
||||
"dsl": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.dsl_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"ftth": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.ftth_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"system": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.system_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"wan": async_redact_data(
|
||||
dataclasses.asdict(await data.system.box.wan_get_info()),
|
||||
TO_REDACT,
|
||||
),
|
||||
"dsl": _async_redact_data(await data.system.box.dsl_get_info()),
|
||||
"ftth": _async_redact_data(await data.system.box.ftth_get_info()),
|
||||
"system": _async_redact_data(await data.system.box.system_get_info()),
|
||||
"wan": _async_redact_data(await data.system.box.wan_get_info()),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sfr_box",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["sfrbox-api==0.0.8"]
|
||||
"requirements": ["sfrbox-api==0.0.11"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sfrbox_api.models import DslInfo, SystemInfo, WanInfo
|
||||
|
||||
@@ -129,7 +130,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
|
||||
"unknown",
|
||||
],
|
||||
translation_key="dsl_line_status",
|
||||
value_fn=lambda x: x.line_status.lower().replace(" ", "_"),
|
||||
value_fn=lambda x: _value_to_option(x.line_status),
|
||||
),
|
||||
SFRBoxSensorEntityDescription[DslInfo](
|
||||
key="training",
|
||||
@@ -149,7 +150,7 @@ DSL_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[DslInfo], ...] = (
|
||||
"unknown",
|
||||
],
|
||||
translation_key="dsl_training",
|
||||
value_fn=lambda x: x.training.lower().replace(" ", "_").replace(".", "_"),
|
||||
value_fn=lambda x: _value_to_option(x.training),
|
||||
),
|
||||
)
|
||||
SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
|
||||
@@ -181,7 +182,7 @@ SYSTEM_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[SystemInfo], ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
value_fn=lambda x: None if x.temperature is None else x.temperature / 1000,
|
||||
value_fn=lambda x: _get_temperature(x.temperature),
|
||||
),
|
||||
)
|
||||
WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
|
||||
@@ -203,23 +204,38 @@ WAN_SENSOR_TYPES: tuple[SFRBoxSensorEntityDescription[WanInfo], ...] = (
|
||||
)
|
||||
|
||||
|
||||
def _value_to_option(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return value
|
||||
return value.lower().replace(" ", "_").replace(".", "_")
|
||||
|
||||
|
||||
def _get_temperature(value: float | None) -> float | None:
|
||||
if value is None or value < 1000:
|
||||
return value
|
||||
return value / 1000
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
) -> None:
|
||||
"""Set up the sensors."""
|
||||
data: DomainData = hass.data[DOMAIN][entry.entry_id]
|
||||
system_info = data.system.data
|
||||
if TYPE_CHECKING:
|
||||
assert system_info is not None
|
||||
|
||||
entities: list[SFRBoxSensor] = [
|
||||
SFRBoxSensor(data.system, description, data.system.data)
|
||||
SFRBoxSensor(data.system, description, system_info)
|
||||
for description in SYSTEM_SENSOR_TYPES
|
||||
]
|
||||
entities.extend(
|
||||
SFRBoxSensor(data.wan, description, data.system.data)
|
||||
SFRBoxSensor(data.wan, description, system_info)
|
||||
for description in WAN_SENSOR_TYPES
|
||||
)
|
||||
if data.system.data.net_infra == "adsl":
|
||||
if system_info.net_infra == "adsl":
|
||||
entities.extend(
|
||||
SFRBoxSensor(data.dsl, description, data.system.data)
|
||||
SFRBoxSensor(data.dsl, description, system_info)
|
||||
for description in DSL_SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -251,4 +267,6 @@ class SFRBoxSensor[_T](CoordinatorEntity[SFRDataUpdateCoordinator[_T]], SensorEn
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the native value of the device."""
|
||||
if self.coordinator.data is None:
|
||||
return None
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
|
||||
@@ -94,8 +94,13 @@ class SmlightConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
mac = discovery_info.properties.get("mac")
|
||||
# fallback for legacy firmware
|
||||
if mac is None:
|
||||
info = await self.client.get_info()
|
||||
try:
|
||||
info = await self.client.get_info()
|
||||
except SmlightConnectionError:
|
||||
# User is likely running unsupported ESPHome firmware
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
mac = info.MAC
|
||||
|
||||
await self.async_set_unique_id(format_mac(mac))
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
@@ -40,6 +42,7 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
|
||||
|
||||
self.unique_id: str | None = None
|
||||
self.client = Api2(host=host, session=async_get_clientsession(hass))
|
||||
self.legacy_api: int = 0
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Authenticate if needed during initial setup."""
|
||||
@@ -60,11 +63,28 @@ class SmDataUpdateCoordinator(DataUpdateCoordinator[SmData]):
|
||||
info = await self.client.get_info()
|
||||
self.unique_id = format_mac(info.MAC)
|
||||
|
||||
if info.legacy_api:
|
||||
self.legacy_api = info.legacy_api
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"unsupported_firmware",
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
learn_more_url="https://smlight.tech/flasher/#SLZB-06",
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="unsupported_firmware",
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> SmData:
|
||||
"""Fetch data from the SMLIGHT device."""
|
||||
try:
|
||||
sensors = Sensors()
|
||||
if not self.legacy_api:
|
||||
sensors = await self.client.get_sensors()
|
||||
|
||||
return SmData(
|
||||
sensors=await self.client.get_sensors(),
|
||||
sensors=sensors,
|
||||
info=await self.client.get_info(),
|
||||
)
|
||||
except SmlightConnectionError as err:
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/smlight",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["pysmlight==0.0.13"],
|
||||
"requirements": ["pysmlight==0.0.14"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -45,5 +45,11 @@
|
||||
"name": "RAM usage"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"unsupported_firmware": {
|
||||
"title": "SLZB core firmware update required",
|
||||
"description": "Your SMLIGHT SLZB-06x device is running an unsupported core firmware version. Please update it to the latest version to enjoy all the features of this integration."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ REPEAT_TO_SONOS = {
|
||||
SONOS_TO_REPEAT = {meaning: mode for mode, meaning in REPEAT_TO_SONOS.items()}
|
||||
|
||||
UPNP_ERRORS_TO_IGNORE = ["701", "711", "712"]
|
||||
ANNOUNCE_NOT_SUPPORTED_ERRORS: list[str] = ["globalError"]
|
||||
|
||||
SERVICE_SNAPSHOT = "snapshot"
|
||||
SERVICE_RESTORE = "restore"
|
||||
@@ -556,11 +557,24 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity):
|
||||
) from exc
|
||||
if response.get("success"):
|
||||
return
|
||||
raise HomeAssistantError(
|
||||
translation_domain=SONOS_DOMAIN,
|
||||
translation_key="announce_media_error",
|
||||
translation_placeholders={"media_id": media_id, "response": response},
|
||||
)
|
||||
if response.get("type") in ANNOUNCE_NOT_SUPPORTED_ERRORS:
|
||||
# If the speaker does not support announce do not raise and
|
||||
# fall through to_play_media to play the clip directly.
|
||||
_LOGGER.debug(
|
||||
"Speaker %s does not support announce, media_id %s response %s",
|
||||
self.speaker.zone_name,
|
||||
media_id,
|
||||
response,
|
||||
)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=SONOS_DOMAIN,
|
||||
translation_key="announce_media_error",
|
||||
translation_placeholders={
|
||||
"media_id": media_id,
|
||||
"response": response,
|
||||
},
|
||||
)
|
||||
|
||||
if spotify.is_spotify_media_type(media_type):
|
||||
media_type = spotify.resolve_spotify_media_type(media_type)
|
||||
|
||||
@@ -10,9 +10,8 @@ import surepy
|
||||
from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN, SURE_API_TIMEOUT
|
||||
@@ -27,57 +26,43 @@ USER_DATA_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
surepy_client = surepy.Surepy(
|
||||
data[CONF_USERNAME],
|
||||
data[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
token = await surepy_client.sac.get_token()
|
||||
|
||||
return {CONF_TOKEN: token}
|
||||
|
||||
|
||||
class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sure Petcare."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize."""
|
||||
self._username: str | None = None
|
||||
reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
info = await validate_input(self.hass, user_input)
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
user_input[CONF_TOKEN] = info[CONF_TOKEN]
|
||||
return self.async_create_entry(
|
||||
title="Sure Petcare",
|
||||
data=user_input,
|
||||
if user_input is not None:
|
||||
client = surepy.Surepy(
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
token = await client.sac.get_token()
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
title="Sure Petcare",
|
||||
data={**user_input, CONF_TOKEN: token},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors
|
||||
@@ -87,18 +72,27 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self._username = entry_data[CONF_USERNAME]
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
assert self.reauth_entry
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
user_input[CONF_USERNAME] = self._username
|
||||
client = surepy.Surepy(
|
||||
self.reauth_entry.data[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
auth_token=None,
|
||||
api_timeout=SURE_API_TIMEOUT,
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
token = await client.sac.get_token()
|
||||
except SurePetcareAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except SurePetcareError:
|
||||
@@ -107,16 +101,20 @@ class SurePetCareConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_USERNAME].lower()
|
||||
return self.async_update_reload_and_abort(
|
||||
self.reauth_entry,
|
||||
data={
|
||||
**self.reauth_entry.data,
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
CONF_TOKEN: token,
|
||||
},
|
||||
)
|
||||
if existing_entry:
|
||||
await self.hass.config_entries.async_reload(existing_entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={"username": self._username},
|
||||
description_placeholders={
|
||||
"username": self.reauth_entry.data[CONF_USERNAME]
|
||||
},
|
||||
data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -7,9 +7,14 @@ import logging
|
||||
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, SERVICE_RELOAD
|
||||
from homeassistant.const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import ConfigEntryError, HomeAssistantError
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.device import (
|
||||
async_remove_stale_devices_links_keep_current_device,
|
||||
@@ -19,7 +24,7 @@ from homeassistant.helpers.service import async_register_admin_service
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import async_get_integration
|
||||
|
||||
from .const import CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, CONF_TRIGGER, DOMAIN, PLATFORMS
|
||||
from .coordinator import TriggerUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -67,6 +72,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.options.get(CONF_DEVICE_ID),
|
||||
)
|
||||
|
||||
for key in (CONF_MAX, CONF_MIN, CONF_STEP):
|
||||
if key not in entry.options:
|
||||
continue
|
||||
if isinstance(entry.options[key], str):
|
||||
raise ConfigEntryError(
|
||||
f"The '{entry.options.get(CONF_NAME) or ""}' number template needs to "
|
||||
f"be reconfigured, {key} must be a number, got '{entry.options[key]}'"
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["template_type"],)
|
||||
)
|
||||
|
||||
@@ -107,15 +107,15 @@ def generate_schema(domain: str, flow_type: str) -> vol.Schema:
|
||||
if domain == Platform.NUMBER:
|
||||
schema |= {
|
||||
vol.Required(CONF_STATE): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_MIN, default=f"{{{{{DEFAULT_MIN_VALUE}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_MAX, default=f"{{{{{DEFAULT_MAX_VALUE}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(
|
||||
CONF_STEP, default=f"{{{{{DEFAULT_STEP}}}}}"
|
||||
): selector.TemplateSelector(),
|
||||
vol.Required(CONF_MIN, default=DEFAULT_MIN_VALUE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Required(CONF_MAX, default=DEFAULT_MAX_VALUE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Required(CONF_STEP, default=DEFAULT_STEP): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(mode=selector.NumberSelectorMode.BOX),
|
||||
),
|
||||
vol.Optional(CONF_SET_VALUE): selector.ActionSelector(),
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,14 @@ PLATFORMS = [
|
||||
Platform.WEATHER,
|
||||
]
|
||||
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_ATTRIBUTE_TEMPLATES = "attribute_templates"
|
||||
CONF_ATTRIBUTES = "attributes"
|
||||
CONF_AVAILABILITY = "availability"
|
||||
CONF_MAX = "max"
|
||||
CONF_MIN = "min"
|
||||
CONF_OBJECT_ID = "object_id"
|
||||
CONF_PICTURE = "picture"
|
||||
CONF_PRESS = "press"
|
||||
CONF_OBJECT_ID = "object_id"
|
||||
CONF_STEP = "step"
|
||||
CONF_TURN_OFF = "turn_off"
|
||||
CONF_TURN_ON = "turn_on"
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.helpers.script import Script
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import TriggerUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
from .const import CONF_MAX, CONF_MIN, CONF_STEP, DOMAIN
|
||||
from .template_entity import (
|
||||
TEMPLATE_ENTITY_AVAILABILITY_SCHEMA,
|
||||
TEMPLATE_ENTITY_ICON_SCHEMA,
|
||||
@@ -42,9 +42,6 @@ from .trigger_entity import TriggerEntity
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_SET_VALUE = "set_value"
|
||||
CONF_MIN = "min"
|
||||
CONF_MAX = "max"
|
||||
CONF_STEP = "step"
|
||||
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
@@ -192,3 +192,10 @@ class TeslemetryWallConnectorEntity(
|
||||
.get(self.din, {})
|
||||
.get(self.key)
|
||||
)
|
||||
|
||||
@property
|
||||
def exists(self) -> bool:
|
||||
"""Return True if it exists in the wall connector coordinator data."""
|
||||
return self.key in self.coordinator.data.get("wall_connectors", {}).get(
|
||||
self.din, {}
|
||||
)
|
||||
|
||||
@@ -379,18 +379,18 @@ ENERGY_LIVE_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(key="island_status", device_class=SensorDeviceClass.ENUM),
|
||||
)
|
||||
|
||||
WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
WALL_CONNECTOR_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = (
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_state",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_fault_state",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="wall_connector_power",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
@@ -398,8 +398,9 @@ WALL_CONNECTOR_DESCRIPTIONS: tuple[SensorEntityDescription, ...] = (
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
TeslemetrySensorEntityDescription(
|
||||
key="vin",
|
||||
value_fn=lambda vin: vin or "disconnected",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -525,13 +526,13 @@ class TeslemetryEnergyLiveSensorEntity(TeslemetryEnergyLiveEntity, SensorEntity)
|
||||
class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorEntity):
|
||||
"""Base class for Teslemetry energy site metric sensors."""
|
||||
|
||||
entity_description: SensorEntityDescription
|
||||
entity_description: TeslemetrySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: TeslemetryEnergyData,
|
||||
din: str,
|
||||
description: SensorEntityDescription,
|
||||
description: TeslemetrySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self.entity_description = description
|
||||
@@ -543,8 +544,8 @@ class TeslemetryWallConnectorSensorEntity(TeslemetryWallConnectorEntity, SensorE
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
"""Update the attributes of the sensor."""
|
||||
self._attr_available = not self.is_none
|
||||
self._attr_native_value = self._value
|
||||
if self.exists:
|
||||
self._attr_native_value = self.entity_description.value_fn(self._value)
|
||||
|
||||
|
||||
class TeslemetryEnergyInfoSensorEntity(TeslemetryEnergyInfoEntity, SensorEntity):
|
||||
|
||||
@@ -420,7 +420,10 @@
|
||||
"name": "version"
|
||||
},
|
||||
"vin": {
|
||||
"name": "Vehicle"
|
||||
"name": "Vehicle",
|
||||
"state": {
|
||||
"disconnected": "Disconnected"
|
||||
}
|
||||
},
|
||||
"vpp_backup_reserve_percent": {
|
||||
"name": "VPP backup reserve"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyTibber==0.30.1"]
|
||||
"requirements": ["pyTibber==0.30.2"]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ALIAS,
|
||||
CONF_AUTHENTICATION,
|
||||
CONF_DEVICE,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_MODEL,
|
||||
@@ -44,8 +45,12 @@ from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
CONF_AES_KEYS,
|
||||
CONF_CONFIG_ENTRY_MINOR_VERSION,
|
||||
CONF_CONNECTION_PARAMETERS,
|
||||
CONF_CREDENTIALS_HASH,
|
||||
CONF_DEVICE_CONFIG,
|
||||
CONF_USES_HTTP,
|
||||
CONNECT_TIMEOUT,
|
||||
DISCOVERY_TIMEOUT,
|
||||
DOMAIN,
|
||||
@@ -85,9 +90,7 @@ def async_trigger_discovery(
|
||||
CONF_ALIAS: device.alias or mac_alias(device.mac),
|
||||
CONF_HOST: device.host,
|
||||
CONF_MAC: formatted_mac,
|
||||
CONF_DEVICE_CONFIG: device.config.to_dict(
|
||||
exclude_credentials=True,
|
||||
),
|
||||
CONF_DEVICE: device,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -136,25 +139,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
host: str = entry.data[CONF_HOST]
|
||||
credentials = await get_credentials(hass)
|
||||
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
|
||||
entry_use_http = entry.data.get(CONF_USES_HTTP, False)
|
||||
entry_aes_keys = entry.data.get(CONF_AES_KEYS)
|
||||
|
||||
config: DeviceConfig | None = None
|
||||
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
|
||||
conn_params: Device.ConnectionParameters | None = None
|
||||
if conn_params_dict := entry.data.get(CONF_CONNECTION_PARAMETERS):
|
||||
try:
|
||||
config = DeviceConfig.from_dict(config_dict)
|
||||
conn_params = Device.ConnectionParameters.from_dict(conn_params_dict)
|
||||
except KasaException:
|
||||
_LOGGER.warning(
|
||||
"Invalid connection type dict for %s: %s", host, config_dict
|
||||
"Invalid connection parameters dict for %s: %s", host, conn_params_dict
|
||||
)
|
||||
|
||||
if not config:
|
||||
config = DeviceConfig(host)
|
||||
else:
|
||||
config.host = host
|
||||
|
||||
config.timeout = CONNECT_TIMEOUT
|
||||
if config.uses_http is True:
|
||||
config.http_client = create_async_tplink_clientsession(hass)
|
||||
|
||||
client = create_async_tplink_clientsession(hass) if entry_use_http else None
|
||||
config = DeviceConfig(
|
||||
host,
|
||||
timeout=CONNECT_TIMEOUT,
|
||||
http_client=client,
|
||||
aes_keys=entry_aes_keys,
|
||||
)
|
||||
if conn_params:
|
||||
config.connection_type = conn_params
|
||||
# If we have in memory credentials use them otherwise check for credentials_hash
|
||||
if credentials:
|
||||
config.credentials = credentials
|
||||
@@ -173,14 +178,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
|
||||
raise ConfigEntryNotReady from ex
|
||||
|
||||
device_credentials_hash = device.credentials_hash
|
||||
device_config_dict = device.config.to_dict(exclude_credentials=True)
|
||||
# Do not store the credentials hash inside the device_config
|
||||
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
|
||||
|
||||
# We not need to update the connection parameters or the use_http here
|
||||
# because if they were wrong we would have failed to connect.
|
||||
# Discovery will update those if necessary.
|
||||
updates: dict[str, Any] = {}
|
||||
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
|
||||
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
|
||||
if device_config_dict != config_dict:
|
||||
updates[CONF_DEVICE_CONFIG] = device_config_dict
|
||||
if entry_aes_keys != device.config.aes_keys:
|
||||
updates[CONF_AES_KEYS] = device.config.aes_keys
|
||||
if entry.data.get(CONF_ALIAS) != device.alias:
|
||||
updates[CONF_ALIAS] = device.alias
|
||||
if entry.data.get(CONF_MODEL) != device.model:
|
||||
@@ -307,12 +313,20 @@ def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
version = config_entry.version
|
||||
minor_version = config_entry.minor_version
|
||||
entry_version = config_entry.version
|
||||
entry_minor_version = config_entry.minor_version
|
||||
# having a condition to check for the current version allows
|
||||
# tests to be written per migration step.
|
||||
config_flow_minor_version = CONF_CONFIG_ENTRY_MINOR_VERSION
|
||||
|
||||
_LOGGER.debug("Migrating from version %s.%s", version, minor_version)
|
||||
|
||||
if version == 1 and minor_version < 3:
|
||||
new_minor_version = 3
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Migrating from version %s.%s", entry_version, entry_minor_version
|
||||
)
|
||||
# Previously entities on child devices added themselves to the parent
|
||||
# device and set their device id as identifiers along with mac
|
||||
# as a connection which creates a single device entry linked by all
|
||||
@@ -359,12 +373,19 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
new_identifiers,
|
||||
)
|
||||
|
||||
minor_version = 3
|
||||
hass.config_entries.async_update_entry(config_entry, minor_version=3)
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, minor_version=new_minor_version
|
||||
)
|
||||
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
|
||||
if version == 1 and minor_version == 3:
|
||||
new_minor_version = 4
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
# credentials_hash stored in the device_config should be moved to data.
|
||||
updates: dict[str, Any] = {}
|
||||
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||
@@ -372,15 +393,44 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
|
||||
updates[CONF_CREDENTIALS_HASH] = credentials_hash
|
||||
updates[CONF_DEVICE_CONFIG] = config_dict
|
||||
minor_version = 4
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**config_entry.data,
|
||||
**updates,
|
||||
},
|
||||
minor_version=minor_version,
|
||||
minor_version=new_minor_version,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
|
||||
|
||||
new_minor_version = 5
|
||||
if (
|
||||
entry_version == 1
|
||||
and entry_minor_version < new_minor_version <= config_flow_minor_version
|
||||
):
|
||||
# complete device config no longer to be stored, only required
|
||||
# attributes like connection parameters and aes_keys
|
||||
updates = {}
|
||||
entry_data = {
|
||||
k: v for k, v in config_entry.data.items() if k != CONF_DEVICE_CONFIG
|
||||
}
|
||||
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
|
||||
assert isinstance(config_dict, dict)
|
||||
if connection_parameters := config_dict.get("connection_type"):
|
||||
updates[CONF_CONNECTION_PARAMETERS] = connection_parameters
|
||||
if (use_http := config_dict.get(CONF_USES_HTTP)) is not None:
|
||||
updates[CONF_USES_HTTP] = use_http
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data={
|
||||
**entry_data,
|
||||
**updates,
|
||||
},
|
||||
minor_version=new_minor_version,
|
||||
)
|
||||
_LOGGER.debug(
|
||||
"Migration to version %s.%s complete", entry_version, new_minor_version
|
||||
)
|
||||
return True
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user