forked from home-assistant/core
Compare commits
127 Commits
2025.4.0b1
...
2025.4.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
360bffa3a9 | ||
|
|
2214d9b330 | ||
|
|
6a2d733d85 | ||
|
|
7392d5a30a | ||
|
|
b3deeca939 | ||
|
|
c38a3a239c | ||
|
|
afa6ed09ef | ||
|
|
deb966128f | ||
|
|
73707fa231 | ||
|
|
10ac39f6b2 | ||
|
|
2e05dc8618 | ||
|
|
d8233b4de5 | ||
|
|
7cbc3ea65f | ||
|
|
6f0a9910ea | ||
|
|
b8793760a1 | ||
|
|
6264f9c67b | ||
|
|
2a74deb84e | ||
|
|
9d1ff37a79 | ||
|
|
2f99164781 | ||
|
|
80ef32f09d | ||
|
|
63be0e2e1a | ||
|
|
74c4553bb0 | ||
|
|
e240707b32 | ||
|
|
7c867852a9 | ||
|
|
2d149dc746 | ||
|
|
7edcddd3e4 | ||
|
|
71f658b560 | ||
|
|
9886db5d6d | ||
|
|
c236cd070c | ||
|
|
9f1a830d32 | ||
|
|
1e69ce9111 | ||
|
|
389297155d | ||
|
|
c341b86520 | ||
|
|
88eef379b2 | ||
|
|
34767d4058 | ||
|
|
12c3d54a63 | ||
|
|
33a185dade | ||
|
|
c1c5776d85 | ||
|
|
eda642554d | ||
|
|
51f5ce013f | ||
|
|
f7794ea6b5 | ||
|
|
7a1bea7ff5 | ||
|
|
c7c645776d | ||
|
|
667cb772e9 | ||
|
|
933d008e52 | ||
|
|
d868f39aea | ||
|
|
28d776a0b0 | ||
|
|
b5d541b596 | ||
|
|
4948499889 | ||
|
|
7696b101f6 | ||
|
|
fd2987a9fd | ||
|
|
4c1d32020a | ||
|
|
b40bdab0ae | ||
|
|
d192aecd3b | ||
|
|
d1781f5766 | ||
|
|
2c4461457a | ||
|
|
82959081de | ||
|
|
acdac6d5e8 | ||
|
|
d3d7889883 | ||
|
|
60ece3e1c9 | ||
|
|
a9f8529460 | ||
|
|
ec53b61f9e | ||
|
|
e9f02edd8b | ||
|
|
d1b7898219 | ||
|
|
8dc21ef619 | ||
|
|
d9f91598a5 | ||
|
|
c540acf2bd | ||
|
|
f702f3efcd | ||
|
|
9410061405 | ||
|
|
485b28d9ea | ||
|
|
d59200a9f5 | ||
|
|
44a92ca81c | ||
|
|
d39fa39a03 | ||
|
|
36ec857523 | ||
|
|
fcb8cdc146 | ||
|
|
2322b0b65f | ||
|
|
87baaf4255 | ||
|
|
b7f0e877f0 | ||
|
|
5d92a04732 | ||
|
|
8ff879df22 | ||
|
|
9fb7ee676e | ||
|
|
2c855a3986 | ||
|
|
cdd4894e30 | ||
|
|
5f26226712 | ||
|
|
8baf61031d | ||
|
|
e90ba40553 | ||
|
|
b38016425f | ||
|
|
ee5e3f7691 | ||
|
|
7af6a4f493 | ||
|
|
c25f26a290 | ||
|
|
8d62cb60a6 | ||
|
|
4f799069ea | ||
|
|
af708b78e0 | ||
|
|
f46e659740 | ||
|
|
7bd517e6ff | ||
|
|
e9abdab1f5 | ||
|
|
86eee4f041 | ||
|
|
9db60c830c | ||
|
|
c43a4682b9 | ||
|
|
2a4996055a | ||
|
|
4643fc2c14 | ||
|
|
6410b90d82 | ||
|
|
e5c00eceae | ||
|
|
fe65579df8 | ||
|
|
281beecb05 | ||
|
|
7546b5d269 | ||
|
|
490e3201b9 | ||
|
|
04be575139 | ||
|
|
854cae7f12 | ||
|
|
109d20978f | ||
|
|
f8d284ec4b | ||
|
|
06ebe0810f | ||
|
|
802ad2ff51 | ||
|
|
9070a8d579 | ||
|
|
e8b2a3de8b | ||
|
|
39549d5dd4 | ||
|
|
0c19e47bd4 | ||
|
|
05507d77e3 | ||
|
|
94558e2d40 | ||
|
|
4f22fe8f7f | ||
|
|
9e7dfbb857 | ||
|
|
02d182239a | ||
|
|
4e0f581747 | ||
|
|
42d97d348c | ||
|
|
69380c85ca | ||
|
|
b38c647830 | ||
|
|
2396fd1090 |
5
homeassistant/brands/eve.json
Normal file
5
homeassistant/brands/eve.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "eve",
|
||||
"name": "Eve",
|
||||
"iot_standards": ["matter"]
|
||||
}
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.9.9"]
|
||||
"requirements": ["aioairzone==1.0.0"]
|
||||
}
|
||||
|
||||
@@ -266,7 +266,7 @@ async def _transform_stream(
|
||||
raise ValueError("Unexpected stop event without a current block")
|
||||
if current_block["type"] == "tool_use":
|
||||
tool_block = cast(ToolUseBlockParam, current_block)
|
||||
tool_args = json.loads(current_tool_args)
|
||||
tool_args = json.loads(current_tool_args) if current_tool_args else {}
|
||||
tool_block["input"] = tool_args
|
||||
yield {
|
||||
"tool_calls": [
|
||||
|
||||
@@ -20,6 +20,7 @@ import voluptuous as vol
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IGNORE,
|
||||
SOURCE_REAUTH,
|
||||
SOURCE_ZEROCONF,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@@ -381,7 +382,9 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_IDENTIFIERS: list(combined_identifiers),
|
||||
},
|
||||
)
|
||||
if entry.source != SOURCE_IGNORE:
|
||||
# Don't reload ignored entries or in the middle of reauth,
|
||||
# e.g. if the user is entering a new PIN
|
||||
if entry.source != SOURCE_IGNORE and self.source != SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
if not allow_exist:
|
||||
raise DeviceAlreadyConfigured
|
||||
|
||||
@@ -120,6 +120,7 @@ class AppleTvMediaPlayer(
|
||||
"""Initialize the Apple TV media player."""
|
||||
super().__init__(name, identifier, manager)
|
||||
self._playing: Playing | None = None
|
||||
self._playing_last_updated: datetime | None = None
|
||||
self._app_list: dict[str, str] = {}
|
||||
|
||||
@callback
|
||||
@@ -209,6 +210,7 @@ class AppleTvMediaPlayer(
|
||||
This is a callback function from pyatv.interface.PushListener.
|
||||
"""
|
||||
self._playing = playstatus
|
||||
self._playing_last_updated = dt_util.utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
@@ -316,7 +318,7 @@ class AppleTvMediaPlayer(
|
||||
def media_position_updated_at(self) -> datetime | None:
|
||||
"""Last valid time of media position."""
|
||||
if self.state in {MediaPlayerState.PLAYING, MediaPlayerState.PAUSED}:
|
||||
return dt_util.utcnow()
|
||||
return self._playing_last_updated
|
||||
return None
|
||||
|
||||
async def async_play_media(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.1"]
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
@@ -53,10 +54,18 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
try:
|
||||
await api.login()
|
||||
except aiocomelit_exceptions.CannotConnect as err:
|
||||
raise CannotConnect from err
|
||||
except (aiocomelit_exceptions.CannotConnect, TimeoutError) as err:
|
||||
raise CannotConnect(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
raise InvalidAuth from err
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
await api.close()
|
||||
|
||||
@@ -162,7 +162,7 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set new target humidity."""
|
||||
if self.mode == HumidifierComelitMode.OFF:
|
||||
if not self._attr_is_on:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="humidity_while_off",
|
||||
@@ -190,9 +190,13 @@ class ComelitHumidifierEntity(CoordinatorEntity[ComelitSerialBridge], Humidifier
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, self._set_command
|
||||
)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.api.set_humidity_status(
|
||||
self._device.index, HumidifierComelitCommand.OFF
|
||||
)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -52,7 +52,9 @@
|
||||
"rest": "Rest",
|
||||
"sabotated": "Sabotated"
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
"humidifier": {
|
||||
"humidifier": {
|
||||
"name": "Humidifier"
|
||||
},
|
||||
@@ -67,6 +69,12 @@
|
||||
},
|
||||
"invalid_clima_data": {
|
||||
"message": "Invalid 'clima' data"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Error connecting: {error}"
|
||||
},
|
||||
"cannot_authenticate": {
|
||||
"message": "Error authenticating: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,4 +81,7 @@ class ComelitSwitchEntity(CoordinatorEntity[ComelitSerialBridge], SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
return self.coordinator.data[OTHER][self._device.index].status == STATE_ON
|
||||
return (
|
||||
self.coordinator.data[self._device.type][self._device.index].status
|
||||
== STATE_ON
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
||||
from homeassistant.util.ssl import client_context_no_verify
|
||||
|
||||
from .const import KEY_MAC, TIMEOUT
|
||||
from .coordinator import DaikinConfigEntry, DaikinCoordinator
|
||||
@@ -48,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
|
||||
key=entry.data.get(CONF_API_KEY),
|
||||
uuid=entry.data.get(CONF_UUID),
|
||||
password=entry.data.get(CONF_PASSWORD),
|
||||
ssl_context=client_context_no_verify(),
|
||||
)
|
||||
_LOGGER.debug("Connection to %s successful", host)
|
||||
except TimeoutError as err:
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PASSWORD, CONF_UUID
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
from homeassistant.util.ssl import client_context_no_verify
|
||||
|
||||
from .const import DOMAIN, KEY_MAC, TIMEOUT
|
||||
|
||||
@@ -90,6 +91,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
key=key,
|
||||
uuid=uuid,
|
||||
password=password,
|
||||
ssl_context=client_context_no_verify(),
|
||||
)
|
||||
except (TimeoutError, ClientError):
|
||||
self.host = None
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.14.1"],
|
||||
"requirements": ["pydaikin==2.15.0"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["devolo_plc_api"],
|
||||
"requirements": ["devolo-plc-api==1.4.1"],
|
||||
"requirements": ["devolo-plc-api==1.5.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_dvl-deviceapi._tcp.local.",
|
||||
|
||||
@@ -179,22 +179,18 @@ class DukeEnergyCoordinator(DataUpdateCoordinator[None]):
|
||||
one = timedelta(days=1)
|
||||
if start_time is None:
|
||||
# Max 3 years of data
|
||||
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
|
||||
if agreement_date is None:
|
||||
start = dt_util.now(tz) - timedelta(days=3 * 365)
|
||||
else:
|
||||
start = max(
|
||||
agreement_date.replace(tzinfo=tz),
|
||||
dt_util.now(tz) - timedelta(days=3 * 365),
|
||||
)
|
||||
start = dt_util.now(tz) - timedelta(days=3 * 365)
|
||||
else:
|
||||
start = datetime.fromtimestamp(start_time, tz=tz) - lookback
|
||||
agreement_date = dt_util.parse_datetime(meter["agreementActiveDate"])
|
||||
if agreement_date is not None:
|
||||
start = max(agreement_date.replace(tzinfo=tz), start)
|
||||
|
||||
start = start.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end = dt_util.now(tz).replace(hour=0, minute=0, second=0, microsecond=0) - one
|
||||
_LOGGER.debug("Data lookup range: %s - %s", start, end)
|
||||
|
||||
start_step = end - lookback
|
||||
start_step = max(end - lookback, start)
|
||||
end_step = end
|
||||
usage: dict[datetime, dict[str, float | int]] = {}
|
||||
while True:
|
||||
|
||||
@@ -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==12.4.0"]
|
||||
"requirements": ["py-sucks==0.9.10", "deebot-client==12.5.0"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"requirements": ["pyenphase==1.25.1"],
|
||||
"requirements": ["pyenphase==1.25.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -35,7 +35,7 @@ async def validate_input(data):
|
||||
lon = weather_data.lon
|
||||
|
||||
return {
|
||||
CONF_TITLE: weather_data.metadata.get("location"),
|
||||
CONF_TITLE: weather_data.metadata.location,
|
||||
CONF_STATION: weather_data.station_id,
|
||||
CONF_LATITUDE: lat,
|
||||
CONF_LONGITUDE: lon,
|
||||
|
||||
@@ -7,7 +7,7 @@ from datetime import timedelta
|
||||
import logging
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from env_canada import ECAirQuality, ECRadar, ECWeather, ec_exc
|
||||
from env_canada import ECAirQuality, ECRadar, ECWeather, ECWeatherUpdateFailed, ec_exc
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -65,6 +65,6 @@ class ECDataUpdateCoordinator[DataT: ECDataType](DataUpdateCoordinator[DataT]):
|
||||
"""Fetch data from EC."""
|
||||
try:
|
||||
await self.ec_data.update()
|
||||
except (ET.ParseError, ec_exc.UnknownStationId) as ex:
|
||||
except (ET.ParseError, ECWeatherUpdateFailed, ec_exc.UnknownStationId) as ex:
|
||||
raise UpdateFailed(f"Error fetching {self.name} data: {ex}") from ex
|
||||
return self.ec_data
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.8.0"]
|
||||
"requirements": ["env-canada==0.10.1"]
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
|
||||
key="timestamp",
|
||||
translation_key="timestamp",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda data: data.metadata.get("timestamp"),
|
||||
value_fn=lambda data: data.metadata.timestamp,
|
||||
),
|
||||
ECSensorEntityDescription(
|
||||
key="uv_index",
|
||||
@@ -289,7 +289,7 @@ class ECBaseSensorEntity[DataT: ECDataType](
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._ec_data = coordinator.ec_data
|
||||
self._attr_attribution = self._ec_data.metadata["attribution"]
|
||||
self._attr_attribution = self._ec_data.metadata.attribution
|
||||
self._attr_unique_id = f"{coordinator.config_entry.title}-{description.key}"
|
||||
self._attr_device_info = coordinator.device_info
|
||||
|
||||
@@ -313,8 +313,8 @@ class ECSensorEntity[DataT: ECDataType](ECBaseSensorEntity[DataT]):
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator, description)
|
||||
self._attr_extra_state_attributes = {
|
||||
ATTR_LOCATION: self._ec_data.metadata.get("location"),
|
||||
ATTR_STATION: self._ec_data.metadata.get("station"),
|
||||
ATTR_LOCATION: self._ec_data.metadata.location,
|
||||
ATTR_STATION: self._ec_data.metadata.station,
|
||||
}
|
||||
|
||||
|
||||
@@ -329,8 +329,8 @@ class ECAlertSensorEntity(ECBaseSensorEntity[ECWeather]):
|
||||
return None
|
||||
|
||||
extra_state_attrs = {
|
||||
ATTR_LOCATION: self._ec_data.metadata.get("location"),
|
||||
ATTR_STATION: self._ec_data.metadata.get("station"),
|
||||
ATTR_LOCATION: self._ec_data.metadata.location,
|
||||
ATTR_STATION: self._ec_data.metadata.station,
|
||||
}
|
||||
for index, alert in enumerate(value, start=1):
|
||||
extra_state_attrs[f"alert_{index}"] = alert.get("title")
|
||||
|
||||
@@ -115,7 +115,7 @@ class ECWeatherEntity(
|
||||
"""Initialize Environment Canada weather."""
|
||||
super().__init__(coordinator)
|
||||
self.ec_data = coordinator.ec_data
|
||||
self._attr_attribution = self.ec_data.metadata["attribution"]
|
||||
self._attr_attribution = self.ec_data.metadata.attribution
|
||||
self._attr_translation_key = "forecast"
|
||||
self._attr_unique_id = _calculate_unique_id(
|
||||
coordinator.config_entry.unique_id, False
|
||||
|
||||
@@ -13,7 +13,7 @@ from aioesphomeapi import (
|
||||
APIConnectionError,
|
||||
APIVersion,
|
||||
DeviceInfo as EsphomeDeviceInfo,
|
||||
EncryptionHelloAPIError,
|
||||
EncryptionPlaintextAPIError,
|
||||
EntityInfo,
|
||||
HomeassistantServiceCall,
|
||||
InvalidAuthAPIError,
|
||||
@@ -571,7 +571,7 @@ class ESPHomeManager:
|
||||
if isinstance(
|
||||
err,
|
||||
(
|
||||
EncryptionHelloAPIError,
|
||||
EncryptionPlaintextAPIError,
|
||||
RequiresEncryptionAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
InvalidAuthAPIError,
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.8.0",
|
||||
"aioesphomeapi==29.9.0",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.12.0"
|
||||
],
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohome", "evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.0.4"]
|
||||
"requirements": ["evohome-async==1.0.5"]
|
||||
}
|
||||
|
||||
@@ -301,6 +301,7 @@ class FibaroController:
|
||||
device.ha_id = (
|
||||
f"{slugify(room_name)}_{slugify(device.name)}_{device.fibaro_id}"
|
||||
)
|
||||
platform = None
|
||||
if device.enabled and (not device.is_plugin or self._import_plugins):
|
||||
platform = self._map_device_to_platform(device)
|
||||
if platform is None:
|
||||
|
||||
@@ -53,5 +53,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flux_led",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["flux_led"],
|
||||
"requirements": ["flux-led==1.1.3"]
|
||||
"requirements": ["flux-led==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/forecast_solar",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["forecast-solar==4.0.0"]
|
||||
"requirements": ["forecast-solar==4.1.0"]
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, DOMAIN, MeshRoles
|
||||
from .const import BUTTON_TYPE_WOL, CONNECTION_TYPE_LAN, MeshRoles
|
||||
from .coordinator import (
|
||||
FRITZ_DATA_KEY,
|
||||
AvmWrapper,
|
||||
@@ -175,16 +175,6 @@ class FritzBoxWOLButton(FritzDeviceBase, ButtonEntity):
|
||||
self._name = f"{self.hostname} Wake on LAN"
|
||||
self._attr_unique_id = f"{self._mac}_wake_on_lan"
|
||||
self._is_available = True
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, self._mac)},
|
||||
default_manufacturer="AVM",
|
||||
default_model="FRITZ!Box Tracked device",
|
||||
default_name=device.hostname,
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
avm_wrapper.unique_id,
|
||||
),
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
|
||||
@@ -526,7 +526,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
def manage_device_info(
|
||||
self, dev_info: Device, dev_mac: str, consider_home: bool
|
||||
) -> bool:
|
||||
"""Update device lists."""
|
||||
"""Update device lists and return if device is new."""
|
||||
_LOGGER.debug("Client dev_info: %s", dev_info)
|
||||
|
||||
if dev_mac in self._devices:
|
||||
@@ -536,6 +536,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
device = FritzDevice(dev_mac, dev_info.name)
|
||||
device.update(dev_info, consider_home)
|
||||
self._devices[dev_mac] = device
|
||||
|
||||
# manually register device entry for new connected device
|
||||
dr.async_get(self.hass).async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
connections={(CONNECTION_NETWORK_MAC, dev_mac)},
|
||||
default_manufacturer="AVM",
|
||||
default_model="FRITZ!Box Tracked device",
|
||||
default_name=device.hostname,
|
||||
via_device=(DOMAIN, self.unique_id),
|
||||
)
|
||||
return True
|
||||
|
||||
async def async_send_signal_device_update(self, new_device: bool) -> None:
|
||||
|
||||
@@ -26,6 +26,9 @@ class FritzDeviceBase(CoordinatorEntity[AvmWrapper]):
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._mac: str = device.mac_address
|
||||
self._name: str = device.hostname or DEFAULT_DEVICE_NAME
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device.mac_address)}
|
||||
)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
|
||||
@@ -7,9 +7,7 @@ rules:
|
||||
config-flow-test-coverage:
|
||||
status: todo
|
||||
comment: one coverage miss in line 110
|
||||
config-flow:
|
||||
status: todo
|
||||
comment: data_description are missing
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
{
|
||||
"common": {
|
||||
"data_description_host": "The hostname or IP address of your FRITZ!Box router.",
|
||||
"data_description_port": "Leave empty to use the default port.",
|
||||
"data_description_username": "Username for the FRITZ!Box.",
|
||||
"data_description_password": "Password for the FRITZ!Box.",
|
||||
"data_description_ssl": "Use SSL to connect to the FRITZ!Box."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
@@ -9,6 +16,11 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
@@ -17,6 +29,10 @@
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||
"password": "[%key:component::fritz::common::data_description_password%]"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
@@ -28,8 +44,9 @@
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router.",
|
||||
"port": "Leave it empty to use the default port."
|
||||
"host": "[%key:component::fritz::common::data_description_host%]",
|
||||
"port": "[%key:component::fritz::common::data_description_port%]",
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
@@ -43,8 +60,11 @@
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your FRITZ!Box router.",
|
||||
"port": "Leave it empty to use the default port."
|
||||
"host": "[%key:component::fritz::common::data_description_host%]",
|
||||
"port": "[%key:component::fritz::common::data_description_port%]",
|
||||
"username": "[%key:component::fritz::common::data_description_username%]",
|
||||
"password": "[%key:component::fritz::common::data_description_password%]",
|
||||
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -70,6 +90,10 @@
|
||||
"data": {
|
||||
"consider_home": "Seconds to consider a device at 'home'",
|
||||
"old_discovery": "Enable old discovery method"
|
||||
},
|
||||
"data_description": {
|
||||
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
|
||||
"old_discovery": "Enable old discovery method. This is needed for some scenarios."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,8 +193,12 @@
|
||||
"config_entry_not_found": {
|
||||
"message": "Failed to perform action \"{service}\". Config entry for target not found"
|
||||
},
|
||||
"service_parameter_unknown": { "message": "Action or parameter unknown" },
|
||||
"service_not_supported": { "message": "Action not supported" },
|
||||
"service_parameter_unknown": {
|
||||
"message": "Action or parameter unknown"
|
||||
},
|
||||
"service_not_supported": {
|
||||
"message": "Action not supported"
|
||||
},
|
||||
"error_refresh_hosts_info": {
|
||||
"message": "Error refreshing hosts info"
|
||||
},
|
||||
|
||||
@@ -511,16 +511,6 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
self._name = f"{device.hostname} Internet Access"
|
||||
self._attr_unique_id = f"{self._mac}_internet_access"
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(CONNECTION_NETWORK_MAC, self._mac)},
|
||||
default_manufacturer="AVM",
|
||||
default_model="FRITZ!Box Tracked device",
|
||||
default_name=device.hostname,
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
avm_wrapper.unique_id,
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
|
||||
@@ -137,6 +137,7 @@ SENSOR_TYPES: Final[tuple[FritzSensorEntityDescription, ...]] = (
|
||||
key="battery",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
suitable=lambda device: device.battery_level is not None,
|
||||
native_value=lambda device: device.battery_level,
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250401.0"]
|
||||
"requirements": ["home-assistant-frontend==20250411.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.0.3"]
|
||||
"requirements": ["gcal-sync==7.0.0", "oauth2client==4.1.3", "ical==9.1.0"]
|
||||
}
|
||||
|
||||
@@ -179,28 +179,30 @@ class GoogleGenerativeAIOptionsFlow(OptionsFlow):
|
||||
) -> ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
|
||||
if user_input[CONF_LLM_HASS_API] == "none":
|
||||
user_input.pop(CONF_LLM_HASS_API)
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
if not (
|
||||
user_input.get(CONF_LLM_HASS_API, "none") != "none"
|
||||
and user_input.get(CONF_USE_GOOGLE_SEARCH_TOOL, False) is True
|
||||
):
|
||||
# Don't allow to save options that enable the Google Seearch tool with an Assist API
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
errors[CONF_USE_GOOGLE_SEARCH_TOOL] = "invalid_google_search_option"
|
||||
|
||||
# Re-render the options again, now with the recommended options shown/hidden
|
||||
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
|
||||
|
||||
options = {
|
||||
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
|
||||
CONF_PROMPT: user_input[CONF_PROMPT],
|
||||
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
|
||||
}
|
||||
options = user_input
|
||||
|
||||
schema = await google_generative_ai_config_option_schema(
|
||||
self.hass, options, self._genai_client
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(schema),
|
||||
step_id="init", data_schema=vol.Schema(schema), errors=errors
|
||||
)
|
||||
|
||||
|
||||
@@ -301,7 +303,7 @@ async def google_generative_ai_config_option_schema(
|
||||
CONF_TEMPERATURE,
|
||||
description={"suggested_value": options.get(CONF_TEMPERATURE)},
|
||||
default=RECOMMENDED_TEMPERATURE,
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
|
||||
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
|
||||
vol.Optional(
|
||||
CONF_TOP_P,
|
||||
description={"suggested_value": options.get(CONF_TOP_P)},
|
||||
|
||||
@@ -55,6 +55,10 @@ from .const import (
|
||||
# Max number of back and forth with the LLM to generate a response
|
||||
MAX_TOOL_ITERATIONS = 10
|
||||
|
||||
ERROR_GETTING_RESPONSE = (
|
||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -429,6 +433,12 @@ class GoogleGenerativeAIConversationEntity(
|
||||
raise HomeAssistantError(
|
||||
f"The message got blocked due to content violations, reason: {chat_response.prompt_feedback.block_reason_message}"
|
||||
)
|
||||
if not chat_response.candidates:
|
||||
LOGGER.error(
|
||||
"No candidates found in the response: %s",
|
||||
chat_response,
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
|
||||
except (
|
||||
APIError,
|
||||
@@ -452,9 +462,7 @@ class GoogleGenerativeAIConversationEntity(
|
||||
|
||||
response_parts = chat_response.candidates[0].content.parts
|
||||
if not response_parts:
|
||||
raise HomeAssistantError(
|
||||
"Sorry, I had a problem getting a response from Google Generative AI."
|
||||
)
|
||||
raise HomeAssistantError(ERROR_GETTING_RESPONSE)
|
||||
content = " ".join(
|
||||
[part.text.strip() for part in response_parts if part.text]
|
||||
)
|
||||
|
||||
@@ -40,9 +40,13 @@
|
||||
"enable_google_search_tool": "Enable Google Search tool"
|
||||
},
|
||||
"data_description": {
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template."
|
||||
"prompt": "Instruct how the LLM should respond. This can be a template.",
|
||||
"enable_google_search_tool": "Only works with \"No control\" in the \"Control Home Assistant\" setting. See docs for a workaround using it with \"Assist\"."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_google_search_option": "Google Search cannot be enabled alongside any Assist capability, this can only be used when Assist is set to \"No control\"."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["growattServer"],
|
||||
"requirements": ["growattServer==1.5.0"]
|
||||
"requirements": ["growattServer==1.6.0"]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ def build_rrule(task: TaskData) -> rrule:
|
||||
|
||||
bysetpos = None
|
||||
if rrule_frequency == MONTHLY and task.weeksOfMonth:
|
||||
bysetpos = task.weeksOfMonth
|
||||
bysetpos = [i + 1 for i in task.weeksOfMonth]
|
||||
weekdays = weekdays if weekdays else [MO]
|
||||
|
||||
return rrule(
|
||||
|
||||
@@ -265,6 +265,11 @@
|
||||
"version_latest": {
|
||||
"name": "Newest version"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
"update": {
|
||||
"name": "[%key:component::update::title%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
|
||||
@@ -39,7 +39,7 @@ from .entity import (
|
||||
from .update_helper import update_addon, update_core
|
||||
|
||||
ENTITY_DESCRIPTION = UpdateEntityDescription(
|
||||
name="Update",
|
||||
translation_key="update",
|
||||
key=ATTR_VERSION_LATEST,
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyheos"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyheos==1.0.4"],
|
||||
"requirements": ["pyheos==1.0.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-denon-com:device:ACT-Denon:1"
|
||||
|
||||
@@ -87,6 +87,7 @@ BASE_SUPPORTED_FEATURES = (
|
||||
|
||||
PLAY_STATE_TO_STATE = {
|
||||
None: MediaPlayerState.IDLE,
|
||||
PlayState.UNKNOWN: MediaPlayerState.IDLE,
|
||||
PlayState.PLAY: MediaPlayerState.PLAYING,
|
||||
PlayState.STOP: MediaPlayerState.IDLE,
|
||||
PlayState.PAUSE: MediaPlayerState.PAUSED,
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.69", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.70", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -73,6 +73,19 @@ class HomeConnectApplianceData:
|
||||
self.settings.update(other.settings)
|
||||
self.status.update(other.status)
|
||||
|
||||
@classmethod
|
||||
def empty(cls, appliance: HomeAppliance) -> HomeConnectApplianceData:
|
||||
"""Return empty data."""
|
||||
return cls(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
|
||||
|
||||
class HomeConnectCoordinator(
|
||||
DataUpdateCoordinator[dict[str, HomeConnectApplianceData]]
|
||||
@@ -191,7 +204,7 @@ class HomeConnectCoordinator(
|
||||
events = self.data[event_message_ha_id].events
|
||||
for event in event_message.data.items:
|
||||
event_key = event.key
|
||||
if event_key in SettingKey:
|
||||
if event_key in SettingKey.__members__.values(): # type: ignore[comparison-overlap]
|
||||
setting_key = SettingKey(event_key)
|
||||
if setting_key in settings:
|
||||
settings[setting_key].value = event.value
|
||||
@@ -228,9 +241,7 @@ class HomeConnectCoordinator(
|
||||
appliance_data = await self._get_appliance_data(
|
||||
appliance_info, self.data.get(appliance_info.ha_id)
|
||||
)
|
||||
if event_message_ha_id in self.data:
|
||||
self.data[event_message_ha_id].update(appliance_data)
|
||||
else:
|
||||
if event_message_ha_id not in self.data:
|
||||
self.data[event_message_ha_id] = appliance_data
|
||||
for listener, context in self._special_listeners.values():
|
||||
if (
|
||||
@@ -358,15 +369,7 @@ class HomeConnectCoordinator(
|
||||
model=appliance.vib,
|
||||
)
|
||||
if appliance.ha_id not in self.data:
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData(
|
||||
commands=set(),
|
||||
events={},
|
||||
info=appliance,
|
||||
options={},
|
||||
programs=[],
|
||||
settings={},
|
||||
status={},
|
||||
)
|
||||
self.data[appliance.ha_id] = HomeConnectApplianceData.empty(appliance)
|
||||
else:
|
||||
self.data[appliance.ha_id].info.connected = appliance.connected
|
||||
old_appliances.remove(appliance.ha_id)
|
||||
@@ -402,6 +405,15 @@ class HomeConnectCoordinator(
|
||||
name=appliance.name,
|
||||
model=appliance.vib,
|
||||
)
|
||||
if not appliance.connected:
|
||||
_LOGGER.debug(
|
||||
"Appliance %s is not connected, skipping data fetch",
|
||||
appliance.ha_id,
|
||||
)
|
||||
if appliance_data_to_update:
|
||||
appliance_data_to_update.info.connected = False
|
||||
return appliance_data_to_update
|
||||
return HomeConnectApplianceData.empty(appliance)
|
||||
try:
|
||||
settings = {
|
||||
setting.key: setting
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
AVAILABLE_MAPS_ENUM,
|
||||
BEAN_AMOUNT_OPTIONS,
|
||||
BEAN_CONTAINER_OPTIONS,
|
||||
@@ -313,7 +312,7 @@ def _get_entities_for_appliance(
|
||||
HomeConnectProgramSelectEntity(entry.runtime_data, appliance, desc)
|
||||
for desc in PROGRAM_SELECT_ENTITY_DESCRIPTIONS
|
||||
]
|
||||
if appliance.info.type in APPLIANCES_WITH_PROGRAMS
|
||||
if appliance.programs
|
||||
else []
|
||||
),
|
||||
*[
|
||||
|
||||
@@ -71,7 +71,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Postpone loading the config entry if the device is missing
|
||||
device_path = entry.data[DEVICE]
|
||||
if not await hass.async_add_executor_job(os.path.exists, device_path):
|
||||
raise ConfigEntryNotReady
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_disconnected",
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, ["update"])
|
||||
|
||||
|
||||
@@ -5,17 +5,21 @@ from __future__ import annotations
|
||||
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .config_flow import HomeAssistantSkyConnectConfigFlow
|
||||
from .const import DOMAIN
|
||||
from .util import get_hardware_variant
|
||||
|
||||
DOCUMENTATION_URL = "https://skyconnect.home-assistant.io/documentation/"
|
||||
EXPECTED_ENTRY_VERSION = (
|
||||
HomeAssistantSkyConnectConfigFlow.VERSION,
|
||||
HomeAssistantSkyConnectConfigFlow.MINOR_VERSION,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
"""Return board info."""
|
||||
entries = hass.config_entries.async_entries(DOMAIN)
|
||||
|
||||
return [
|
||||
HardwareInfo(
|
||||
board=None,
|
||||
@@ -31,4 +35,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
|
||||
url=DOCUMENTATION_URL,
|
||||
)
|
||||
for entry in entries
|
||||
# Ignore unmigrated config entries in the hardware page
|
||||
if (entry.version, entry.minor_version) == EXPECTED_ENTRY_VERSION
|
||||
]
|
||||
|
||||
@@ -195,5 +195,10 @@
|
||||
"run_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::run_zigbee_flasher_addon%]",
|
||||
"uninstall_zigbee_flasher_addon": "[%key:component::homeassistant_hardware::firmware_picker::options::progress::uninstall_zigbee_flasher_addon%]"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"device_disconnected": {
|
||||
"message": "The device is not plugged in"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -659,13 +659,7 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
|
||||
# e.g. a thermostat is "heating" a room to 75 degrees Fahrenheit.
|
||||
# Can be 0 - 2 (Off, Heat, Cool)
|
||||
|
||||
# If the HVAC is switched off, it must be idle
|
||||
# This works around a bug in some devices (like Eve radiator valves) that
|
||||
# return they are heating when they are not.
|
||||
target = self.service.value(CharacteristicsTypes.HEATING_COOLING_TARGET)
|
||||
if target == HeatingCoolingTargetValues.OFF:
|
||||
return HVACAction.IDLE
|
||||
|
||||
value = self.service.value(CharacteristicsTypes.HEATING_COOLING_CURRENT)
|
||||
current_hass_value = CURRENT_MODE_HOMEKIT_TO_HASS.get(value)
|
||||
|
||||
@@ -679,6 +673,12 @@ class HomeKitClimateEntity(HomeKitBaseClimateEntity):
|
||||
):
|
||||
return HVACAction.FAN
|
||||
|
||||
# If the HVAC is switched off, it must be idle
|
||||
# This works around a bug in some devices (like Eve radiator valves) that
|
||||
# return they are heating when they are not.
|
||||
if target == HeatingCoolingTargetValues.OFF:
|
||||
return HVACAction.IDLE
|
||||
|
||||
return current_hass_value
|
||||
|
||||
@property
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.13"],
|
||||
"requirements": ["aiohomekit==3.2.14"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -197,5 +197,11 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_effect_none": {
|
||||
"title": "Light turned on with deprecated effect",
|
||||
"description": "A light was turned on with the deprecated effect `None`. This has been replaced with `off`. Please update any automations, scenes, or scripts that use this effect."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from ..bridge import HueBridge
|
||||
@@ -44,6 +45,9 @@ FALLBACK_MIN_KELVIN = 6500
|
||||
FALLBACK_MAX_KELVIN = 2000
|
||||
FALLBACK_KELVIN = 5800 # halfway
|
||||
|
||||
# HA 2025.4 replaced the deprecated effect "None" with HA default "off"
|
||||
DEPRECATED_EFFECT_NONE = "None"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -233,6 +237,23 @@ class HueLight(HueBaseEntity, LightEntity):
|
||||
self._color_temp_active = color_temp is not None
|
||||
flash = kwargs.get(ATTR_FLASH)
|
||||
effect = effect_str = kwargs.get(ATTR_EFFECT)
|
||||
if effect_str == DEPRECATED_EFFECT_NONE:
|
||||
# deprecated effect "None" is now "off"
|
||||
effect_str = EFFECT_OFF
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
"deprecated_effect_none",
|
||||
breaks_in_ha_version="2025.10.0",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_effect_none",
|
||||
)
|
||||
self.logger.warning(
|
||||
"Detected deprecated effect 'None' in %s, use 'off' instead. "
|
||||
"This will stop working in HA 2025.10",
|
||||
self.entity_id,
|
||||
)
|
||||
if effect_str == EFFECT_OFF:
|
||||
# ignore effect if set to "off" and we have no effect active
|
||||
# the special effect "off" is only used to stop an active effect
|
||||
|
||||
@@ -136,6 +136,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
|
||||
# Process new device
|
||||
new_devices = current_devices - self._devices_last_update
|
||||
if new_devices:
|
||||
self.data = data
|
||||
_LOGGER.debug("New devices found: %s", ", ".join(map(str, new_devices)))
|
||||
self._add_new_devices(new_devices)
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
},
|
||||
"data_description": {
|
||||
"host": "Hostname or IP-address of the Intergas gateway.",
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
"username": "The username to log in to the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log in to the gateway, is printed at the bottom of the gateway or is `intergas` for some older devices."
|
||||
}
|
||||
},
|
||||
"dhcp_auth": {
|
||||
@@ -22,8 +22,8 @@
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"username": "The username to log into the gateway. This is `admin` in most cases.",
|
||||
"password": "The password to log into the gateway, is printed at the bottom of the Gateway or is `intergas` for some older devices."
|
||||
"username": "[%key:component::incomfort::config::step::user::data_description::username%]",
|
||||
"password": "[%key:component::incomfort::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"dhcp_confirm": {
|
||||
|
||||
@@ -61,11 +61,14 @@ OPTIONS_SCHEMA = vol.Schema(
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
|
||||
async def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
|
||||
default_location = {
|
||||
CONF_LATITUDE: hass.config.latitude,
|
||||
CONF_LONGITUDE: hass.config.longitude,
|
||||
}
|
||||
get_timezones: list[str] = list(
|
||||
await hass.async_add_executor_job(zoneinfo.available_timezones)
|
||||
)
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_DIASPORA, default=DEFAULT_DIASPORA): BooleanSelector(),
|
||||
@@ -75,9 +78,7 @@ def _get_data_schema(hass: HomeAssistant) -> vol.Schema:
|
||||
vol.Optional(CONF_LOCATION, default=default_location): LocationSelector(),
|
||||
vol.Optional(CONF_ELEVATION, default=hass.config.elevation): int,
|
||||
vol.Optional(CONF_TIME_ZONE, default=hass.config.time_zone): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=sorted(zoneinfo.available_timezones()),
|
||||
)
|
||||
SelectSelectorConfig(options=get_timezones, sort=True)
|
||||
),
|
||||
}
|
||||
)
|
||||
@@ -109,7 +110,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
_get_data_schema(self.hass), user_input
|
||||
await _get_data_schema(self.hass), user_input
|
||||
),
|
||||
)
|
||||
|
||||
@@ -121,7 +122,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not user_input:
|
||||
return self.async_show_form(
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
_get_data_schema(self.hass),
|
||||
await _get_data_schema(self.hass),
|
||||
reconfigure_entry.data,
|
||||
),
|
||||
step_id="reconfigure",
|
||||
|
||||
@@ -145,7 +145,10 @@ class KrakenData:
|
||||
await asyncio.sleep(CALL_RATE_LIMIT_SLEEP)
|
||||
|
||||
def _get_websocket_name_asset_pairs(self) -> str:
|
||||
return ",".join(wsname for wsname in self.tradable_asset_pairs.values())
|
||||
return ",".join(
|
||||
self.tradable_asset_pairs[tracked_pair]
|
||||
for tracked_pair in self._config_entry.options[CONF_TRACKED_ASSET_PAIRS]
|
||||
)
|
||||
|
||||
def set_update_interval(self, update_interval: int) -> None:
|
||||
"""Set the coordinator update_interval to the supplied update_interval."""
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENTITIES,
|
||||
CONF_SOURCE,
|
||||
@@ -49,6 +50,7 @@ DEVICE_CLASS_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.METERPERSECOND: SensorDeviceClass.SPEED,
|
||||
pypck.lcn_defs.VarUnit.VOLT: SensorDeviceClass.VOLTAGE,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: SensorDeviceClass.CURRENT,
|
||||
pypck.lcn_defs.VarUnit.PPM: SensorDeviceClass.CO2,
|
||||
}
|
||||
|
||||
UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
@@ -60,6 +62,7 @@ UNIT_OF_MEASUREMENT_MAPPING = {
|
||||
pypck.lcn_defs.VarUnit.METERPERSECOND: UnitOfSpeed.METERS_PER_SECOND,
|
||||
pypck.lcn_defs.VarUnit.VOLT: UnitOfElectricPotential.VOLT,
|
||||
pypck.lcn_defs.VarUnit.AMPERE: UnitOfElectricCurrent.AMPERE,
|
||||
pypck.lcn_defs.VarUnit.PPM: CONCENTRATION_PARTS_PER_MILLION,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -35,5 +35,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/led_ble",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.6"]
|
||||
"requirements": ["bluetooth-data-tools==1.26.5", "led-ble==1.1.7"]
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ turn_on:
|
||||
example: "[255, 100, 100]"
|
||||
selector:
|
||||
color_rgb:
|
||||
kelvin: &kelvin
|
||||
color_temp_kelvin: &color_temp_kelvin
|
||||
filter: *color_temp_support
|
||||
selector:
|
||||
color_temp:
|
||||
@@ -317,7 +317,7 @@ toggle:
|
||||
fields:
|
||||
transition: *transition
|
||||
rgb_color: *rgb_color
|
||||
kelvin: *kelvin
|
||||
color_temp_kelvin: *color_temp_kelvin
|
||||
brightness_pct: *brightness_pct
|
||||
effect: *effect
|
||||
advanced_fields:
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"field_flash_name": "Flash",
|
||||
"field_hs_color_description": "Color in hue/sat format. A list of two integers. Hue is 0-360 and Sat is 0-100.",
|
||||
"field_hs_color_name": "Hue/Sat color",
|
||||
"field_kelvin_description": "Color temperature in Kelvin.",
|
||||
"field_kelvin_name": "Color temperature",
|
||||
"field_color_temp_kelvin_description": "Color temperature in Kelvin.",
|
||||
"field_color_temp_kelvin_name": "Color temperature",
|
||||
"field_profile_description": "Name of a light profile to use.",
|
||||
"field_profile_name": "Profile",
|
||||
"field_rgb_color_description": "The color in RGB format. A list of three integers between 0 and 255 representing the values of red, green, and blue.",
|
||||
@@ -322,9 +322,9 @@
|
||||
"name": "[%key:component::light::common::field_color_temp_name%]",
|
||||
"description": "[%key:component::light::common::field_color_temp_description%]"
|
||||
},
|
||||
"kelvin": {
|
||||
"name": "[%key:component::light::common::field_kelvin_name%]",
|
||||
"description": "[%key:component::light::common::field_kelvin_description%]"
|
||||
"color_temp_kelvin": {
|
||||
"name": "[%key:component::light::common::field_color_temp_kelvin_name%]",
|
||||
"description": "[%key:component::light::common::field_color_temp_kelvin_description%]"
|
||||
},
|
||||
"brightness": {
|
||||
"name": "[%key:component::light::common::field_brightness_name%]",
|
||||
@@ -420,9 +420,9 @@
|
||||
"name": "[%key:component::light::common::field_color_temp_name%]",
|
||||
"description": "[%key:component::light::common::field_color_temp_description%]"
|
||||
},
|
||||
"kelvin": {
|
||||
"name": "[%key:component::light::common::field_kelvin_name%]",
|
||||
"description": "[%key:component::light::common::field_kelvin_description%]"
|
||||
"color_temp_kelvin": {
|
||||
"name": "[%key:component::light::common::field_color_temp_kelvin_name%]",
|
||||
"description": "[%key:component::light::common::field_color_temp_kelvin_description%]"
|
||||
},
|
||||
"brightness": {
|
||||
"name": "[%key:component::light::common::field_brightness_name%]",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -120,6 +121,8 @@ SERVICE_PLAY_PRESET_SCHEMA = cv.make_entity_service_schema(
|
||||
)
|
||||
|
||||
RETRY_POLL_MAXIMUM = 3
|
||||
SCAN_INTERVAL = timedelta(seconds=5)
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/livisi",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["livisi==0.0.24"]
|
||||
"requirements": ["livisi==0.0.25"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.0.3"]
|
||||
"requirements": ["ical==9.1.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.0.3"]
|
||||
"requirements": ["ical==9.1.0"]
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ ATTR_NEXT_RAIN_DT_REF = "forecast_time_ref"
|
||||
|
||||
|
||||
CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire"],
|
||||
ATTR_CONDITION_CLEAR_NIGHT: ["Nuit Claire", "Nuit claire", "Ciel clair"],
|
||||
ATTR_CONDITION_CLOUDY: ["Très nuageux", "Couvert"],
|
||||
ATTR_CONDITION_FOG: [
|
||||
"Brume ou bancs de brouillard",
|
||||
@@ -48,9 +48,10 @@ CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
"Brouillard",
|
||||
"Brouillard givrant",
|
||||
"Bancs de Brouillard",
|
||||
"Brouillard dense",
|
||||
],
|
||||
ATTR_CONDITION_HAIL: ["Risque de grêle", "Risque de grèle"],
|
||||
ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages"],
|
||||
ATTR_CONDITION_LIGHTNING: ["Risque d'orages", "Orages", "Orage avec grêle"],
|
||||
ATTR_CONDITION_LIGHTNING_RAINY: [
|
||||
"Pluie orageuses",
|
||||
"Pluies orageuses",
|
||||
@@ -62,6 +63,7 @@ CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
"Éclaircies",
|
||||
"Eclaircies",
|
||||
"Peu nuageux",
|
||||
"Variable",
|
||||
],
|
||||
ATTR_CONDITION_POURING: ["Pluie forte"],
|
||||
ATTR_CONDITION_RAINY: [
|
||||
@@ -74,6 +76,7 @@ CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
"Pluie modérée",
|
||||
"Pluie / Averses",
|
||||
"Averses",
|
||||
"Averses faibles",
|
||||
"Pluie",
|
||||
],
|
||||
ATTR_CONDITION_SNOWY: [
|
||||
@@ -81,6 +84,8 @@ CONDITION_CLASSES: dict[str, list[str]] = {
|
||||
"Neige",
|
||||
"Averses de neige",
|
||||
"Neige forte",
|
||||
"Neige faible",
|
||||
"Averses de neige faible",
|
||||
"Quelques flocons",
|
||||
],
|
||||
ATTR_CONDITION_SNOWY_RAINY: ["Pluie et neige", "Pluie verglaçante"],
|
||||
|
||||
@@ -1611,6 +1611,7 @@ def async_is_pem_data(data: bytes) -> bool:
|
||||
return (
|
||||
b"-----BEGIN CERTIFICATE-----" in data
|
||||
or b"-----BEGIN PRIVATE KEY-----" in data
|
||||
or b"-----BEGIN EC PRIVATE KEY-----" in data
|
||||
or b"-----BEGIN RSA PRIVATE KEY-----" in data
|
||||
or b"-----BEGIN ENCRYPTED PRIVATE KEY-----" in data
|
||||
)
|
||||
|
||||
@@ -154,18 +154,14 @@ def get_origin_support_url(discovery_payload: MQTTDiscoveryPayload) -> str | Non
|
||||
|
||||
@callback
|
||||
def async_log_discovery_origin_info(
|
||||
message: str, discovery_payload: MQTTDiscoveryPayload, level: int = logging.INFO
|
||||
message: str, discovery_payload: MQTTDiscoveryPayload
|
||||
) -> None:
|
||||
"""Log information about the discovery and origin."""
|
||||
# We only log origin info once per device discovery
|
||||
if not _LOGGER.isEnabledFor(level):
|
||||
# bail out early if logging is disabled
|
||||
if not _LOGGER.isEnabledFor(logging.DEBUG):
|
||||
# bail out early if debug logging is disabled
|
||||
return
|
||||
_LOGGER.log(
|
||||
level,
|
||||
"%s%s",
|
||||
message,
|
||||
get_origin_log_string(discovery_payload, include_url=True),
|
||||
_LOGGER.debug(
|
||||
"%s%s", message, get_origin_log_string(discovery_payload, include_url=True)
|
||||
)
|
||||
|
||||
|
||||
@@ -258,7 +254,7 @@ def _generate_device_config(
|
||||
comp_config = config[CONF_COMPONENTS]
|
||||
for platform, discover_id in mqtt_data.discovery_already_discovered:
|
||||
ids = discover_id.split(" ")
|
||||
component_node_id = ids.pop(0)
|
||||
component_node_id = f"{ids.pop(1)} {ids.pop(0)}" if len(ids) > 2 else ids.pop(0)
|
||||
component_object_id = " ".join(ids)
|
||||
if not ids:
|
||||
continue
|
||||
@@ -562,7 +558,7 @@ async def async_start( # noqa: C901
|
||||
elif already_discovered:
|
||||
# Dispatch update
|
||||
message = f"Component has already been discovered: {component} {discovery_id}, sending update"
|
||||
async_log_discovery_origin_info(message, payload, logging.DEBUG)
|
||||
async_log_discovery_origin_info(message, payload)
|
||||
async_dispatcher_send(
|
||||
hass, MQTT_DISCOVERY_UPDATED.format(*discovery_hash), payload
|
||||
)
|
||||
|
||||
@@ -70,8 +70,8 @@ MQTT_NUMBER_ATTRIBUTES_BLOCKED = frozenset(
|
||||
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
"""Validate that the configuration is valid, throws if it isn't."""
|
||||
if config[CONF_MIN] >= config[CONF_MAX]:
|
||||
raise vol.Invalid(f"'{CONF_MAX}' must be > '{CONF_MIN}'")
|
||||
if config[CONF_MIN] > config[CONF_MAX]:
|
||||
raise vol.Invalid(f"{CONF_MAX} must be >= {CONF_MIN}")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@@ -151,6 +151,11 @@ async def async_setup_entry(
|
||||
assert event.object_id is not None
|
||||
if event.object_id in added_ids:
|
||||
return
|
||||
player = mass.players.get(event.object_id)
|
||||
if TYPE_CHECKING:
|
||||
assert player is not None
|
||||
if not player.expose_to_ha:
|
||||
return
|
||||
added_ids.add(event.object_id)
|
||||
async_add_entities([MusicAssistantPlayer(mass, event.object_id)])
|
||||
|
||||
@@ -159,6 +164,8 @@ async def async_setup_entry(
|
||||
mass_players = []
|
||||
# add all current players
|
||||
for player in mass.players:
|
||||
if not player.expose_to_ha:
|
||||
continue
|
||||
added_ids.add(player.player_id)
|
||||
mass_players.append(MusicAssistantPlayer(mass, player.player_id))
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.9.0"]
|
||||
"requirements": ["opower==0.11.1"]
|
||||
}
|
||||
|
||||
@@ -84,8 +84,10 @@
|
||||
"options": {
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||
|
||||
@@ -139,14 +139,13 @@ def query_circular_mean(table: type[StatisticsBase]) -> tuple[Label, Label]:
|
||||
# in Python.
|
||||
# https://en.wikipedia.org/wiki/Circular_mean
|
||||
radians = func.radians(table.mean)
|
||||
weighted_sum_sin = func.sum(func.sin(radians) * table.mean_weight)
|
||||
weighted_sum_cos = func.sum(func.cos(radians) * table.mean_weight)
|
||||
weight = func.sqrt(
|
||||
func.power(func.sum(func.sin(radians) * table.mean_weight), 2)
|
||||
+ func.power(func.sum(func.cos(radians) * table.mean_weight), 2)
|
||||
func.power(weighted_sum_sin, 2) + func.power(weighted_sum_cos, 2)
|
||||
)
|
||||
return (
|
||||
func.degrees(
|
||||
func.atan2(func.sum(func.sin(radians)), func.sum(func.cos(radians)))
|
||||
).label("mean"),
|
||||
func.degrees(func.atan2(weighted_sum_sin, weighted_sum_cos)).label("mean"),
|
||||
weight.label("mean_weight"),
|
||||
)
|
||||
|
||||
@@ -240,18 +239,20 @@ DEG_TO_RAD = math.pi / 180
|
||||
RAD_TO_DEG = 180 / math.pi
|
||||
|
||||
|
||||
def weighted_circular_mean(values: Iterable[tuple[float, float]]) -> float:
|
||||
"""Return the weighted circular mean of the values."""
|
||||
sin_sum = sum(math.sin(x * DEG_TO_RAD) * weight for x, weight in values)
|
||||
cos_sum = sum(math.cos(x * DEG_TO_RAD) * weight for x, weight in values)
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
def weighted_circular_mean(
|
||||
values: Iterable[tuple[float, float]],
|
||||
) -> tuple[float, float]:
|
||||
"""Return the weighted circular mean and the weight of the values."""
|
||||
weighted_sin_sum, weighted_cos_sum = 0.0, 0.0
|
||||
for x, weight in values:
|
||||
rad_x = x * DEG_TO_RAD
|
||||
weighted_sin_sum += math.sin(rad_x) * weight
|
||||
weighted_cos_sum += math.cos(rad_x) * weight
|
||||
|
||||
|
||||
def circular_mean(values: list[float]) -> float:
|
||||
"""Return the circular mean of the values."""
|
||||
sin_sum = sum(math.sin(x * DEG_TO_RAD) for x in values)
|
||||
cos_sum = sum(math.cos(x * DEG_TO_RAD) for x in values)
|
||||
return (RAD_TO_DEG * math.atan2(sin_sum, cos_sum)) % 360
|
||||
return (
|
||||
(RAD_TO_DEG * math.atan2(weighted_sin_sum, weighted_cos_sum)) % 360,
|
||||
math.sqrt(weighted_sin_sum**2 + weighted_cos_sum**2),
|
||||
)
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -300,6 +301,7 @@ class StatisticsRow(BaseStatisticsRow, total=False):
|
||||
min: float | None
|
||||
max: float | None
|
||||
mean: float | None
|
||||
mean_weight: float | None
|
||||
change: float | None
|
||||
|
||||
|
||||
@@ -1023,7 +1025,7 @@ def _reduce_statistics(
|
||||
_want_sum = "sum" in types
|
||||
for statistic_id, stat_list in stats.items():
|
||||
max_values: list[float] = []
|
||||
mean_values: list[float] = []
|
||||
mean_values: list[tuple[float, float]] = []
|
||||
min_values: list[float] = []
|
||||
prev_stat: StatisticsRow = stat_list[0]
|
||||
fake_entry: StatisticsRow = {"start": stat_list[-1]["start"] + period_seconds}
|
||||
@@ -1039,12 +1041,15 @@ def _reduce_statistics(
|
||||
}
|
||||
if _want_mean:
|
||||
row["mean"] = None
|
||||
row["mean_weight"] = None
|
||||
if mean_values:
|
||||
match metadata[statistic_id][1]["mean_type"]:
|
||||
case StatisticMeanType.ARITHMETIC:
|
||||
row["mean"] = mean(mean_values)
|
||||
row["mean"] = mean([x[0] for x in mean_values])
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
row["mean"] = circular_mean(mean_values)
|
||||
row["mean"], row["mean_weight"] = (
|
||||
weighted_circular_mean(mean_values)
|
||||
)
|
||||
mean_values.clear()
|
||||
if _want_min:
|
||||
row["min"] = min(min_values) if min_values else None
|
||||
@@ -1063,7 +1068,8 @@ def _reduce_statistics(
|
||||
max_values.append(_max)
|
||||
if _want_mean:
|
||||
if (_mean := statistic.get("mean")) is not None:
|
||||
mean_values.append(_mean)
|
||||
_mean_weight = statistic.get("mean_weight") or 0.0
|
||||
mean_values.append((_mean, _mean_weight))
|
||||
if _want_min and (_min := statistic.get("min")) is not None:
|
||||
min_values.append(_min)
|
||||
prev_stat = statistic
|
||||
@@ -1385,7 +1391,7 @@ def _get_max_mean_min_statistic(
|
||||
match metadata[1]["mean_type"]:
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
if circular_means := max_mean_min["circular_means"]:
|
||||
mean_value = weighted_circular_mean(circular_means)
|
||||
mean_value = weighted_circular_mean(circular_means)[0]
|
||||
case StatisticMeanType.ARITHMETIC:
|
||||
if (mean_value := max_mean_min.get("mean_acc")) is not None and (
|
||||
duration := max_mean_min.get("duration")
|
||||
@@ -1739,12 +1745,12 @@ def statistic_during_period(
|
||||
|
||||
|
||||
_type_column_mapping = {
|
||||
"last_reset": "last_reset_ts",
|
||||
"max": "max",
|
||||
"mean": "mean",
|
||||
"min": "min",
|
||||
"state": "state",
|
||||
"sum": "sum",
|
||||
"last_reset": ("last_reset_ts",),
|
||||
"max": ("max",),
|
||||
"mean": ("mean", "mean_weight"),
|
||||
"min": ("min",),
|
||||
"state": ("state",),
|
||||
"sum": ("sum",),
|
||||
}
|
||||
|
||||
|
||||
@@ -1756,12 +1762,13 @@ def _generate_select_columns_for_types_stmt(
|
||||
track_on: list[str | None] = [
|
||||
table.__tablename__, # type: ignore[attr-defined]
|
||||
]
|
||||
for key, column in _type_column_mapping.items():
|
||||
if key in types:
|
||||
columns = columns.add_columns(getattr(table, column))
|
||||
track_on.append(column)
|
||||
else:
|
||||
track_on.append(None)
|
||||
for key, type_columns in _type_column_mapping.items():
|
||||
for column in type_columns:
|
||||
if key in types:
|
||||
columns = columns.add_columns(getattr(table, column))
|
||||
track_on.append(column)
|
||||
else:
|
||||
track_on.append(None)
|
||||
return lambda_stmt(lambda: columns, track_on=track_on)
|
||||
|
||||
|
||||
@@ -1944,6 +1951,12 @@ def _statistics_during_period_with_session(
|
||||
hass, session, start_time, units, _types, table, metadata, result
|
||||
)
|
||||
|
||||
# filter out mean_weight as it is only needed to reduce statistics
|
||||
# and not needed in the result
|
||||
for stats_rows in result.values():
|
||||
for row in stats_rows:
|
||||
row.pop("mean_weight", None)
|
||||
|
||||
# Return statistics combined with metadata
|
||||
return result
|
||||
|
||||
@@ -2391,7 +2404,12 @@ def _sorted_statistics_to_dict(
|
||||
field_map["last_reset"] = field_map.pop("last_reset_ts")
|
||||
sum_idx = field_map["sum"] if "sum" in types else None
|
||||
sum_only = len(types) == 1 and sum_idx is not None
|
||||
row_mapping = tuple((key, field_map[key]) for key in types if key in field_map)
|
||||
row_mapping = tuple(
|
||||
(column, field_map[column])
|
||||
for key in types
|
||||
for column in ({key, *_type_column_mapping.get(key, ())})
|
||||
if column in field_map
|
||||
)
|
||||
# Append all statistic entries, and optionally do unit conversion
|
||||
table_duration_seconds = table.duration.total_seconds()
|
||||
for meta_id, db_rows in stats_by_meta_id.items():
|
||||
|
||||
@@ -69,7 +69,10 @@ class RemoteCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
except CalendarParseError as err:
|
||||
errors["base"] = "invalid_ics_file"
|
||||
_LOGGER.debug("Invalid .ics file: %s", err)
|
||||
_LOGGER.error("Error reading the calendar information: %s", err.message)
|
||||
_LOGGER.debug(
|
||||
"Additional calendar error detail: %s", str(err.detailed_error)
|
||||
)
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CALENDAR_NAME], data=user_input
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.0.3"]
|
||||
"requirements": ["ical==9.1.0"]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"forbidden": "The server understood the request but refuses to authorize it.",
|
||||
"invalid_ics_file": "[%key:component::local_calendar::config::error::invalid_ics_file%]"
|
||||
"invalid_ics_file": "There was a problem reading the calendar information. See the error log for additional details."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"sensor": {
|
||||
"charge_state": {
|
||||
"default": "mdi:mdi:flash-off",
|
||||
"default": "mdi:flash-off",
|
||||
"state": {
|
||||
"charge_in_progress": "mdi:flash"
|
||||
}
|
||||
|
||||
@@ -371,6 +371,9 @@ def migrate_entity_ids(
|
||||
new_device_id = f"{host.unique_id}"
|
||||
else:
|
||||
new_device_id = f"{host.unique_id}_{device_uid[1]}"
|
||||
_LOGGER.debug(
|
||||
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
|
||||
)
|
||||
new_identifiers = {(DOMAIN, new_device_id)}
|
||||
device_reg.async_update_device(device.id, new_identifiers=new_identifiers)
|
||||
|
||||
@@ -383,6 +386,9 @@ def migrate_entity_ids(
|
||||
new_device_id = f"{host.unique_id}_{host.api.camera_uid(ch)}"
|
||||
else:
|
||||
new_device_id = f"{device_uid[0]}_{host.api.camera_uid(ch)}"
|
||||
_LOGGER.debug(
|
||||
"Updating Reolink device UID from %s to %s", device_uid, new_device_id
|
||||
)
|
||||
new_identifiers = {(DOMAIN, new_device_id)}
|
||||
existing_device = device_reg.async_get_device(identifiers=new_identifiers)
|
||||
if existing_device is None:
|
||||
@@ -415,13 +421,31 @@ def migrate_entity_ids(
|
||||
host.unique_id
|
||||
):
|
||||
new_id = f"{host.unique_id}_{entity.unique_id.split('_', 1)[1]}"
|
||||
_LOGGER.debug(
|
||||
"Updating Reolink entity unique_id from %s to %s",
|
||||
entity.unique_id,
|
||||
new_id,
|
||||
)
|
||||
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
|
||||
|
||||
if entity.device_id in ch_device_ids:
|
||||
ch = ch_device_ids[entity.device_id]
|
||||
id_parts = entity.unique_id.split("_", 2)
|
||||
if len(id_parts) < 3:
|
||||
_LOGGER.warning(
|
||||
"Reolink channel %s entity has unexpected unique_id format %s, with device id %s",
|
||||
ch,
|
||||
entity.unique_id,
|
||||
entity.device_id,
|
||||
)
|
||||
continue
|
||||
if host.api.supported(ch, "UID") and id_parts[1] != host.api.camera_uid(ch):
|
||||
new_id = f"{host.unique_id}_{host.api.camera_uid(ch)}_{id_parts[2]}"
|
||||
_LOGGER.debug(
|
||||
"Updating Reolink entity unique_id from %s to %s",
|
||||
entity.unique_id,
|
||||
new_id,
|
||||
)
|
||||
existing_entity = entity_reg.async_get_entity_id(
|
||||
entity.domain, entity.platform, new_id
|
||||
)
|
||||
|
||||
@@ -301,7 +301,7 @@ async def async_setup_entry(
|
||||
)
|
||||
for entity_description in BINARY_SMART_AI_SENSORS
|
||||
for location in api.baichuan.smart_location_list(
|
||||
channel, entity_description.key
|
||||
channel, entity_description.smart_type
|
||||
)
|
||||
if entity_description.supported(api, channel, location)
|
||||
)
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.0"]
|
||||
"requirements": ["reolink-aio==0.13.2"]
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class ReolinkVODMediaSource(MediaSource):
|
||||
host = get_host(self.hass, config_entry_id)
|
||||
|
||||
def get_vod_type() -> VodRequestType:
|
||||
if filename.endswith((".mp4", ".vref")):
|
||||
if filename.endswith((".mp4", ".vref")) or host.api.is_hub:
|
||||
if host.api.is_nvr:
|
||||
return VodRequestType.DOWNLOAD
|
||||
return VodRequestType.PLAYBACK
|
||||
|
||||
@@ -79,11 +79,15 @@ def get_device_uid_and_ch(
|
||||
device: dr.DeviceEntry, host: ReolinkHost
|
||||
) -> tuple[list[str], int | None, bool]:
|
||||
"""Get the channel and the split device_uid from a reolink DeviceEntry."""
|
||||
device_uid = [
|
||||
dev_id[1].split("_") for dev_id in device.identifiers if dev_id[0] == DOMAIN
|
||||
][0]
|
||||
|
||||
device_uid = []
|
||||
is_chime = False
|
||||
|
||||
for dev_id in device.identifiers:
|
||||
if dev_id[0] == DOMAIN:
|
||||
device_uid = dev_id[1].split("_")
|
||||
if device_uid[0] == host.unique_id:
|
||||
break
|
||||
|
||||
if len(device_uid) < 2:
|
||||
# NVR itself
|
||||
ch = None
|
||||
|
||||
@@ -153,6 +153,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
ImageConfig(scale=MAP_SCALE),
|
||||
[],
|
||||
)
|
||||
self.last_update_state: str | None = None
|
||||
|
||||
@cached_property
|
||||
def dock_device_info(self) -> DeviceInfo:
|
||||
@@ -225,7 +226,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
"""Update the currently selected map."""
|
||||
# The current map was set in the props update, so these can be done without
|
||||
# worry of applying them to the wrong map.
|
||||
if self.current_map is None:
|
||||
if self.current_map is None or self.current_map not in self.maps:
|
||||
# This exists as a safeguard/ to keep mypy happy.
|
||||
return
|
||||
try:
|
||||
@@ -291,7 +292,6 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
|
||||
async def _async_update_data(self) -> DeviceProp:
|
||||
"""Update data via library."""
|
||||
previous_state = self.roborock_device_info.props.status.state_name
|
||||
try:
|
||||
# Update device props and standard api information
|
||||
await self._update_device_prop()
|
||||
@@ -302,13 +302,17 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
# If the vacuum is currently cleaning and it has been IMAGE_CACHE_INTERVAL
|
||||
# since the last map update, you can update the map.
|
||||
new_status = self.roborock_device_info.props.status
|
||||
if self.current_map is not None and (
|
||||
(
|
||||
new_status.in_cleaning
|
||||
and (dt_util.utcnow() - self.maps[self.current_map].last_updated)
|
||||
> IMAGE_CACHE_INTERVAL
|
||||
if (
|
||||
self.current_map is not None
|
||||
and (current_map := self.maps.get(self.current_map))
|
||||
and (
|
||||
(
|
||||
new_status.in_cleaning
|
||||
and (dt_util.utcnow() - current_map.last_updated)
|
||||
> IMAGE_CACHE_INTERVAL
|
||||
)
|
||||
or self.last_update_state != new_status.state_name
|
||||
)
|
||||
or previous_state != new_status.state_name
|
||||
):
|
||||
try:
|
||||
await self.update_map()
|
||||
@@ -330,6 +334,7 @@ class RoborockDataUpdateCoordinator(DataUpdateCoordinator[DeviceProp]):
|
||||
self.update_interval = V1_CLOUD_NOT_CLEANING_INTERVAL
|
||||
else:
|
||||
self.update_interval = V1_LOCAL_NOT_CLEANING_INTERVAL
|
||||
self.last_update_state = self.roborock_device_info.props.status.state_name
|
||||
return self.roborock_device_info.props
|
||||
|
||||
def _set_current_map(self) -> None:
|
||||
|
||||
@@ -381,7 +381,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity):
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Return the currently valid rooms."""
|
||||
if self.coordinator.current_map is not None:
|
||||
if (
|
||||
self.coordinator.current_map is not None
|
||||
and self.coordinator.current_map in self.coordinator.maps
|
||||
):
|
||||
return list(
|
||||
self.coordinator.maps[self.coordinator.current_map].rooms.values()
|
||||
)
|
||||
@@ -390,7 +393,10 @@ class RoborockCurrentRoom(RoborockCoordinatedEntityV1, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the value reported by the sensor."""
|
||||
if self.coordinator.current_map is not None:
|
||||
if (
|
||||
self.coordinator.current_map is not None
|
||||
and self.coordinator.current_map in self.coordinator.maps
|
||||
):
|
||||
return self.coordinator.maps[self.coordinator.current_map].current_room
|
||||
return None
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from . import trigger
|
||||
from .const import DOMAIN
|
||||
from .helpers import (
|
||||
async_get_client_by_device_entry,
|
||||
async_get_device_entry_by_device_id,
|
||||
@@ -75,4 +76,8 @@ async def async_attach_trigger(
|
||||
hass, trigger_config, action, trigger_info
|
||||
)
|
||||
|
||||
raise HomeAssistantError(f"Unhandled trigger type {trigger_type}")
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unhandled_trigger_type",
|
||||
translation_placeholders={"trigger_type": trigger_type},
|
||||
)
|
||||
|
||||
@@ -106,5 +106,7 @@ class SamsungTVEntity(CoordinatorEntity[SamsungTVDataUpdateCoordinator], Entity)
|
||||
self.entity_id,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
f"Entity {self.entity_id} does not support this service."
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="service_unsupported",
|
||||
translation_placeholders={"entity": self.entity_id},
|
||||
)
|
||||
|
||||
@@ -47,5 +47,13 @@
|
||||
"trigger_type": {
|
||||
"samsungtv.turn_on": "Device is requested to turn on"
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"unhandled_trigger_type": {
|
||||
"message": "Unhandled trigger type {trigger_type}."
|
||||
},
|
||||
"service_unsupported": {
|
||||
"message": "Entity {entity} does not support this action."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ def _time_weighted_arithmetic_mean(
|
||||
|
||||
def _time_weighted_circular_mean(
|
||||
fstates: list[tuple[float, State]], start: datetime.datetime, end: datetime.datetime
|
||||
) -> float:
|
||||
) -> tuple[float, float]:
|
||||
"""Calculate a time weighted circular mean.
|
||||
|
||||
The circular mean is calculated by weighting the states by duration in seconds between
|
||||
@@ -623,7 +623,7 @@ def compile_statistics( # noqa: C901
|
||||
valid_float_states, start, end
|
||||
)
|
||||
case StatisticMeanType.CIRCULAR:
|
||||
stat["mean"] = _time_weighted_circular_mean(
|
||||
stat["mean"], stat["mean_weight"] = _time_weighted_circular_mean(
|
||||
valid_float_states, start, end
|
||||
)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sharkiq"],
|
||||
"requirements": ["sharkiq==1.0.2"]
|
||||
"requirements": ["sharkiq==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from aioshelly.exceptions import (
|
||||
CustomPortNotSupported,
|
||||
DeviceConnectionError,
|
||||
InvalidAuthError,
|
||||
InvalidHostError,
|
||||
MacAddressMismatchError,
|
||||
)
|
||||
from aioshelly.rpc_device import RpcDevice
|
||||
@@ -157,6 +158,8 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.info = await self._async_get_info(host, port)
|
||||
except DeviceConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidHostError:
|
||||
errors["base"] = "invalid_host"
|
||||
except Exception: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
|
||||
@@ -209,7 +209,7 @@ KELVIN_MIN_VALUE_COLOR: Final = 3000
|
||||
BLOCK_WRONG_SLEEP_PERIOD = 21600
|
||||
BLOCK_EXPECTED_SLEEP_PERIOD = 43200
|
||||
|
||||
UPTIME_DEVIATION: Final = 5
|
||||
UPTIME_DEVIATION: Final = 60
|
||||
|
||||
# Time to wait before reloading entry upon device config change
|
||||
ENTRY_RELOAD_COOLDOWN = 60
|
||||
@@ -277,3 +277,7 @@ ROLE_TO_DEVICE_CLASS_MAP = {
|
||||
"current_humidity": SensorDeviceClass.HUMIDITY,
|
||||
"current_temperature": SensorDeviceClass.TEMPERATURE,
|
||||
}
|
||||
|
||||
# We want to check only the first 5 KB of the script if it contains emitEvent()
|
||||
# so that the integration startup remains fast.
|
||||
MAX_SCRIPT_SIZE = 5120
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"requirements": ["aioshelly==13.4.0"],
|
||||
"requirements": ["aioshelly==13.4.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"firmware_not_fully_provisioned": "Device not fully provisioned. Please contact Shelly support",
|
||||
"custom_port_not_supported": "Gen1 device does not support custom port.",
|
||||
|
||||
@@ -58,6 +58,7 @@ from .const import (
|
||||
GEN2_BETA_RELEASE_URL,
|
||||
GEN2_RELEASE_URL,
|
||||
LOGGER,
|
||||
MAX_SCRIPT_SIZE,
|
||||
RPC_INPUTS_EVENTS_TYPES,
|
||||
SHAIR_MAX_WORK_HOURS,
|
||||
SHBTN_INPUTS_EVENTS_TYPES,
|
||||
@@ -199,8 +200,18 @@ def get_device_uptime(uptime: float, last_uptime: datetime | None) -> datetime:
|
||||
|
||||
if (
|
||||
not last_uptime
|
||||
or abs((delta_uptime - last_uptime).total_seconds()) > UPTIME_DEVIATION
|
||||
or (diff := abs((delta_uptime - last_uptime).total_seconds()))
|
||||
> UPTIME_DEVIATION
|
||||
):
|
||||
if last_uptime:
|
||||
LOGGER.debug(
|
||||
"Time deviation %s > %s: uptime=%s, last_uptime=%s, delta_uptime=%s",
|
||||
diff,
|
||||
UPTIME_DEVIATION,
|
||||
uptime,
|
||||
last_uptime,
|
||||
delta_uptime,
|
||||
)
|
||||
return delta_uptime
|
||||
|
||||
return last_uptime
|
||||
@@ -642,7 +653,7 @@ def get_rpc_ws_url(hass: HomeAssistant) -> str | None:
|
||||
|
||||
async def get_rpc_script_event_types(device: RpcDevice, id: int) -> list[str]:
|
||||
"""Return a list of event types for a specific script."""
|
||||
code_response = await device.script_getcode(id)
|
||||
code_response = await device.script_getcode(id, bytes_to_read=MAX_SCRIPT_SIZE)
|
||||
matches = SHELLY_EMIT_EVENT_PATTERN.finditer(code_response["data"])
|
||||
return sorted([*{str(event_type.group(1)) for event_type in matches}])
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import FullDevice, SmartThingsConfigEntry
|
||||
from .const import MAIN
|
||||
from .const import INVALID_SWITCH_CATEGORIES, MAIN
|
||||
from .entity import SmartThingsEntity
|
||||
from .util import deprecate_entity
|
||||
|
||||
@@ -59,10 +59,11 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
Category.DOOR: BinarySensorDeviceClass.DOOR,
|
||||
Category.WINDOW: BinarySensorDeviceClass.WINDOW,
|
||||
},
|
||||
exists_fn=lambda key: key in {"freezer", "cooler"},
|
||||
exists_fn=lambda key: key in {"freezer", "cooler", "cvroom"},
|
||||
component_translation_key={
|
||||
"freezer": "freezer_door",
|
||||
"cooler": "cooler_door",
|
||||
"cvroom": "cool_select_plus_door",
|
||||
},
|
||||
deprecated_fn=(
|
||||
lambda status: "fridge_door"
|
||||
@@ -127,14 +128,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
key=Attribute.SWITCH,
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
is_on_key="on",
|
||||
category={
|
||||
Category.CLOTHING_CARE_MACHINE,
|
||||
Category.COOKTOP,
|
||||
Category.DISHWASHER,
|
||||
Category.DRYER,
|
||||
Category.MICROWAVE,
|
||||
Category.WASHER,
|
||||
},
|
||||
category=INVALID_SWITCH_CATEGORIES,
|
||||
)
|
||||
},
|
||||
Capability.TAMPER_ALERT: {
|
||||
|
||||
@@ -333,7 +333,6 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
"""Define a SmartThings Air Conditioner."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_preset_mode = None
|
||||
|
||||
def __init__(self, client: SmartThings, device: FullDevice) -> None:
|
||||
"""Init the class."""
|
||||
@@ -545,6 +544,18 @@ class SmartThingsAirConditioner(SmartThingsEntity, ClimateEntity):
|
||||
SWING_OFF,
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the preset mode."""
|
||||
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):
|
||||
mode = self.get_attribute_value(
|
||||
Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE,
|
||||
Attribute.AC_OPTIONAL_MODE,
|
||||
)
|
||||
if mode == WINDFREE:
|
||||
return WINDFREE
|
||||
return None
|
||||
|
||||
def _determine_preset_modes(self) -> list[str] | None:
|
||||
"""Return a list of available preset modes."""
|
||||
if self.supports_capability(Capability.CUSTOM_AIR_CONDITIONER_OPTIONAL_MODE):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Constants used by the SmartThings component and platforms."""
|
||||
|
||||
from pysmartthings import Attribute, Capability
|
||||
from pysmartthings import Attribute, Capability, Category
|
||||
|
||||
DOMAIN = "smartthings"
|
||||
|
||||
@@ -109,3 +109,12 @@ SENSOR_ATTRIBUTES_TO_CAPABILITIES: dict[str, str] = {
|
||||
Attribute.WASHER_MODE: Capability.WASHER_MODE,
|
||||
Attribute.WASHER_JOB_STATE: Capability.WASHER_OPERATING_STATE,
|
||||
}
|
||||
|
||||
INVALID_SWITCH_CATEGORIES = {
|
||||
Category.CLOTHING_CARE_MACHINE,
|
||||
Category.COOKTOP,
|
||||
Category.DRYER,
|
||||
Category.WASHER,
|
||||
Category.MICROWAVE,
|
||||
Category.DISHWASHER,
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user