mirror of
https://github.com/home-assistant/core.git
synced 2025-10-16 15:19:35 +00:00
Compare commits
100 Commits
mqtt-subsc
...
2025.10.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
40d7f2a89e | ||
![]() |
13b717e2da | ||
![]() |
5fcfd3ad84 | ||
![]() |
324a7b5443 | ||
![]() |
491ae8f72c | ||
![]() |
259247892f | ||
![]() |
caeda0ef64 | ||
![]() |
df35c535e4 | ||
![]() |
f93b9e0ed0 | ||
![]() |
48a3372cf2 | ||
![]() |
d84fd72428 | ||
![]() |
e8cb386962 | ||
![]() |
5ac726703c | ||
![]() |
688649a799 | ||
![]() |
c5359ade3e | ||
![]() |
4e60dedc1b | ||
![]() |
221d74f83a | ||
![]() |
fbbb3d6415 | ||
![]() |
8297019011 | ||
![]() |
61715dcff3 | ||
![]() |
32b822ee99 | ||
![]() |
e6c2e0ad80 | ||
![]() |
1314427dc5 | ||
![]() |
bf499a45f7 | ||
![]() |
b955e22628 | ||
![]() |
1b222ff5fd | ||
![]() |
f0510e703f | ||
![]() |
cbe3956e15 | ||
![]() |
4588e9da8d | ||
![]() |
5445890fdf | ||
![]() |
9b49f77f86 | ||
![]() |
566c8fb786 | ||
![]() |
b36150c213 | ||
![]() |
809070d2ad | ||
![]() |
f4339dc031 | ||
![]() |
f3b37d24b0 | ||
![]() |
4c8348caa7 | ||
![]() |
b9e7c102ea | ||
![]() |
69d9fa89b7 | ||
![]() |
6f3f5a5ec1 | ||
![]() |
5ecfeca90a | ||
![]() |
00e0570fd4 | ||
![]() |
5a5b94f3af | ||
![]() |
34f00d9b33 | ||
![]() |
4cabc5b368 | ||
![]() |
4045125422 | ||
![]() |
d7393af76f | ||
![]() |
ad41386b27 | ||
![]() |
62d17ea20c | ||
![]() |
c4954731d0 | ||
![]() |
647723d3f0 | ||
![]() |
51c500e22c | ||
![]() |
f6fc13c1f2 | ||
![]() |
0009a7a042 | ||
![]() |
a3d1aa28e7 | ||
![]() |
9f53eb9b76 | ||
![]() |
f53a205ff3 | ||
![]() |
d08517c3df | ||
![]() |
d7398a44a1 | ||
![]() |
9acfc0cb88 | ||
![]() |
1b3d21523a | ||
![]() |
1d407d1326 | ||
![]() |
013346cead | ||
![]() |
5abaabc9da | ||
![]() |
32481312c3 | ||
![]() |
bdc9eb37d3 | ||
![]() |
e0afcbc02b | ||
![]() |
cd56a6a98d | ||
![]() |
9d85893bbb | ||
![]() |
9e8a70225f | ||
![]() |
96ec795d5e | ||
![]() |
65b796070d | ||
![]() |
32994812e5 | ||
![]() |
66ff9d63a3 | ||
![]() |
b2a63d4996 | ||
![]() |
f9f37b7f2a | ||
![]() |
7bdd9dd38a | ||
![]() |
1e8aae0a89 | ||
![]() |
cf668e9dc2 | ||
![]() |
2e91c8700f | ||
![]() |
9d14627daa | ||
![]() |
73b8283748 | ||
![]() |
edeaaa2e63 | ||
![]() |
d26dd8fc39 | ||
![]() |
34640ea735 | ||
![]() |
46a2e21ef0 | ||
![]() |
508af53e72 | ||
![]() |
5f7440608c | ||
![]() |
0d1aa38a26 | ||
![]() |
929f8c148a | ||
![]() |
92db1f5a04 | ||
![]() |
e66b5ce0bf | ||
![]() |
1e17150e9f | ||
![]() |
792902de3d | ||
![]() |
04d78c3dd5 | ||
![]() |
5c8d5bfb84 | ||
![]() |
99bff31869 | ||
![]() |
d949119fb0 | ||
![]() |
e7b737ece5 | ||
![]() |
fb8ddac2e8 |
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -760,8 +760,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||
/homeassistant/components/intesishome/ @jnimmo
|
||||
/homeassistant/components/iometer/ @MaestroOnICe
|
||||
/tests/components/iometer/ @MaestroOnICe
|
||||
/homeassistant/components/iometer/ @jukrebs
|
||||
/tests/components/iometer/ @jukrebs
|
||||
/homeassistant/components/ios/ @robbiet480
|
||||
/tests/components/ios/ @robbiet480
|
||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||
build_from:
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.09.3
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.09.3
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.3
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.3
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.3
|
||||
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
|
||||
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
|
||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
|
||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
|
||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
|
||||
codenotary:
|
||||
signer: notary@home-assistant.io
|
||||
base_image: notary@home-assistant.io
|
||||
|
@@ -71,4 +71,4 @@ POLLEN_CATEGORY_MAP = {
|
||||
}
|
||||
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=10)
|
||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(hours=30)
|
||||
UPDATE_INTERVAL_HOURLY_FORECAST = timedelta(minutes=30)
|
||||
|
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"air_quality": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"cloud_ceiling": {
|
||||
"default": "mdi:weather-fog"
|
||||
},
|
||||
@@ -34,9 +37,6 @@
|
||||
"thunderstorm_probability_night": {
|
||||
"default": "mdi:weather-lightning"
|
||||
},
|
||||
"translation_key": {
|
||||
"default": "mdi:air-filter"
|
||||
},
|
||||
"tree_pollen": {
|
||||
"default": "mdi:tree-outline"
|
||||
},
|
||||
|
@@ -1,7 +1,9 @@
|
||||
"""Airgradient Update platform."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from airgradient import AirGradientConnectionError
|
||||
from propcache.api import cached_property
|
||||
|
||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
SCAN_INTERVAL = timedelta(hours=1)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Representation of Airgradient Update."""
|
||||
|
||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||
_server_unreachable_logged = False
|
||||
|
||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
||||
"""Return the installed version of the entity."""
|
||||
return self.coordinator.data.measures.firmware_version
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return super().available and self._attr_available
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
try:
|
||||
self._attr_latest_version = (
|
||||
await self.coordinator.client.get_latest_firmware_version(
|
||||
self.coordinator.serial_number
|
||||
)
|
||||
)
|
||||
)
|
||||
except AirGradientConnectionError:
|
||||
self._attr_latest_version = None
|
||||
self._attr_available = False
|
||||
if not self._server_unreachable_logged:
|
||||
_LOGGER.error(
|
||||
"Unable to connect to AirGradient server to check for updates"
|
||||
)
|
||||
self._server_unreachable_logged = True
|
||||
else:
|
||||
self._server_unreachable_logged = False
|
||||
self._attr_available = True
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["airos==0.5.1"]
|
||||
"requirements": ["airos==0.5.5"]
|
||||
}
|
||||
|
@@ -2,17 +2,14 @@
|
||||
|
||||
from airtouch4pyapi import AirTouch
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import AirtouchDataUpdateCoordinator
|
||||
from .coordinator import AirTouch4ConfigEntry, AirtouchDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
|
||||
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
|
||||
"""Set up AirTouch4 from a config entry."""
|
||||
@@ -22,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) ->
|
||||
info = airtouch.GetAcs()
|
||||
if not info:
|
||||
raise ConfigEntryNotReady
|
||||
coordinator = AirtouchDataUpdateCoordinator(hass, airtouch)
|
||||
coordinator = AirtouchDataUpdateCoordinator(hass, entry, airtouch)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
@@ -2,26 +2,34 @@
|
||||
|
||||
import logging
|
||||
|
||||
from airtouch4pyapi import AirTouch
|
||||
from airtouch4pyapi.airtouch import AirTouchStatus
|
||||
|
||||
from homeassistant.components.climate import SCAN_INTERVAL
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
||||
|
||||
|
||||
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Airtouch data."""
|
||||
|
||||
def __init__(self, hass, airtouch):
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: AirTouch4ConfigEntry, airtouch: AirTouch
|
||||
) -> None:
|
||||
"""Initialize global Airtouch data updater."""
|
||||
self.airtouch = airtouch
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import async_update_unique_id
|
||||
@@ -51,11 +53,47 @@ BINARY_SENSORS: Final = (
|
||||
),
|
||||
is_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||
is_available_fn=lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
DEPRECATED_BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="babyCryDetectionState",
|
||||
translation_key="baby_cry_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="beepingApplianceDetectionState",
|
||||
translation_key="beeping_appliance_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="coughDetectionState",
|
||||
translation_key="cough_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="dogBarkDetectionState",
|
||||
translation_key="dog_bark_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="waterSoundsDetectionState",
|
||||
translation_key="water_sounds_detection",
|
||||
is_on_fn=lambda device, key: False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -66,6 +104,8 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Replace unique id for "detectionState" binary sensor
|
||||
await async_update_unique_id(
|
||||
hass,
|
||||
@@ -75,6 +115,16 @@ async def async_setup_entry(
|
||||
"detectionState",
|
||||
)
|
||||
|
||||
# Clean up deprecated sensors
|
||||
for sensor_desc in DEPRECATED_BINARY_SENSORS:
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-{sensor_desc.key}"
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, unique_id
|
||||
):
|
||||
_LOGGER.debug("Removing deprecated entity %s", entity_id)
|
||||
entity_registry.async_remove(entity_id)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==6.2.7"]
|
||||
"requirements": ["aioamazondevices==6.4.0"]
|
||||
}
|
||||
|
@@ -32,7 +32,9 @@ class AmazonSensorEntityDescription(SensorEntityDescription):
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
)
|
||||
|
||||
|
||||
@@ -40,9 +42,9 @@ SENSORS: Final = (
|
||||
AmazonSensorEntityDescription(
|
||||
key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement_fn=lambda device, _key: (
|
||||
native_unit_of_measurement_fn=lambda device, key: (
|
||||
UnitOfTemperature.CELSIUS
|
||||
if device.sensors[_key].scale == "CELSIUS"
|
||||
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
|
||||
else UnitOfTemperature.FAHRENHEIT
|
||||
),
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
from .utils import alexa_api_call, async_update_unique_id
|
||||
from .utils import (
|
||||
alexa_api_call,
|
||||
async_remove_dnd_from_virtual_group,
|
||||
async_update_unique_id,
|
||||
)
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
||||
device.online and device.sensors[key].error is False
|
||||
device.online
|
||||
and (sensor := device.sensors.get(key)) is not None
|
||||
and sensor.error is False
|
||||
)
|
||||
method: str
|
||||
|
||||
@@ -58,6 +64,9 @@ async def async_setup_entry(
|
||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
||||
)
|
||||
|
||||
# Remove DND switch from virtual groups
|
||||
await async_remove_dnd_from_virtual_group(hass, coordinator)
|
||||
|
||||
known_devices: set[str] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
|
@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.entity_registry as er
|
||||
@@ -61,3 +63,21 @@ async def async_update_unique_id(
|
||||
|
||||
# Update the registry with the new unique_id
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
|
||||
|
||||
async def async_remove_dnd_from_virtual_group(
|
||||
hass: HomeAssistant,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
) -> None:
|
||||
"""Remove entity DND from virtual group."""
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
for serial_num in coordinator.data:
|
||||
unique_id = f"{serial_num}-do_not_disturb"
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
DOMAIN, SWITCH_DOMAIN, unique_id
|
||||
)
|
||||
is_group = coordinator.data[serial_num].device_family == SPEAKER_GROUP_FAMILY
|
||||
if entity_id and is_group:
|
||||
entity_registry.async_remove(entity_id)
|
||||
_LOGGER.debug("Removed DND switch from virtual group %s", entity_id)
|
||||
|
@@ -8,7 +8,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||
"requirements": ["brother==5.1.0"],
|
||||
"requirements": ["brother==5.1.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_printer._tcp.local.",
|
||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from asyncio.exceptions import TimeoutError
|
||||
from collections.abc import Mapping
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from aiocomelit import (
|
||||
@@ -27,25 +28,20 @@ from .utils import async_client_session
|
||||
DEFAULT_HOST = "192.168.1.252"
|
||||
DEFAULT_PIN = "111111"
|
||||
|
||||
|
||||
pin_regex = r"^[0-9]{4,10}$"
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
|
||||
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
||||
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
|
||||
STEP_RECONFIGURE = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Required(CONF_PORT): cv.port,
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.matches_regex(pin_regex),
|
||||
vol.Optional(CONF_PIN, default=DEFAULT_PIN): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -55,6 +51,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
api: ComelitCommonApi
|
||||
|
||||
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
|
||||
raise InvalidPin
|
||||
|
||||
session = await async_client_session(hass)
|
||||
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||
api = ComeliteSerialBridgeApi(
|
||||
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except InvalidPin:
|
||||
errors["base"] = "invalid_pin"
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
|
||||
|
||||
class InvalidAuth(HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
|
||||
class InvalidPin(HomeAssistantError):
|
||||
"""Error to indicate an invalid pin."""
|
||||
|
@@ -161,7 +161,7 @@ class ComelitSerialBridge(
|
||||
entry: ComelitConfigEntry,
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
pin: str,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
||||
entry: ComelitConfigEntry,
|
||||
host: str,
|
||||
port: int,
|
||||
pin: int,
|
||||
pin: str,
|
||||
session: ClientSession,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
|
@@ -7,7 +7,14 @@ from typing import Any, cast
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
||||
|
||||
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
|
||||
from homeassistant.components.cover import (
|
||||
STATE_CLOSED,
|
||||
STATE_CLOSING,
|
||||
STATE_OPEN,
|
||||
STATE_OPENING,
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
@@ -62,7 +69,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
super().__init__(coordinator, device, config_entry_entry_id)
|
||||
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
||||
self._last_action: int | None = None
|
||||
self._last_state: str | None = None
|
||||
|
||||
def _current_action(self, action: str) -> bool:
|
||||
"""Return the current cover action."""
|
||||
@@ -98,7 +104,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
@bridge_api_call
|
||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||
"""Set desired cover state."""
|
||||
self._last_state = self.state
|
||||
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
||||
self.coordinator.data[COVER][self._device.index].status = state
|
||||
self.async_write_ha_state()
|
||||
@@ -124,5 +129,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
||||
|
||||
await super().async_added_to_hass()
|
||||
|
||||
if last_state := await self.async_get_last_state():
|
||||
self._last_state = last_state.state
|
||||
if (state := await self.async_get_last_state()) is not None:
|
||||
if state.state == STATE_CLOSED:
|
||||
self._last_action = STATE_COVER.index(STATE_CLOSING)
|
||||
if state.state == STATE_OPEN:
|
||||
self._last_action = STATE_COVER.index(STATE_OPENING)
|
||||
|
||||
self._attr_is_closed = state.state == STATE_CLOSED
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==0.12.3"]
|
||||
"requirements": ["aiocomelit==1.1.1"]
|
||||
}
|
||||
|
@@ -43,11 +43,13 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "The provided PIN is invalid. It must be a 4-10 digit number.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"invalid_pin": "[%key:component::comelit::config::abort::invalid_pin%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
|
@@ -514,7 +514,7 @@ class ChatLog:
|
||||
"""Set the LLM system prompt."""
|
||||
llm_api: llm.APIInstance | None = None
|
||||
|
||||
if user_llm_hass_api is None:
|
||||
if not user_llm_hass_api:
|
||||
pass
|
||||
elif isinstance(user_llm_hass_api, llm.API):
|
||||
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycync==0.4.0"]
|
||||
"requirements": ["pycync==0.4.1"]
|
||||
}
|
||||
|
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.16.0"],
|
||||
"requirements": ["pydaikin==2.17.1"],
|
||||
"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.11", "deebot-client==15.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.1.0"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
"requirements": ["env-canada==0.11.3"]
|
||||
}
|
||||
|
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20251001.0"]
|
||||
"requirements": ["home-assistant-frontend==20251001.2"]
|
||||
}
|
||||
|
@@ -76,10 +76,6 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
for service_name in hass.services.async_services_for_domain(DOMAIN):
|
||||
hass.services.async_remove(DOMAIN, service_name)
|
||||
|
||||
conversation.async_unset_agent(hass, entry)
|
||||
|
||||
return True
|
||||
|
@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -68,7 +68,13 @@ async def async_send_text_commands(
|
||||
) -> list[CommandResponse]:
|
||||
"""Send text commands to Google Assistant Service."""
|
||||
# There can only be 1 entry (config_flow has single_instance_allowed)
|
||||
entry: GoogleAssistantSDKConfigEntry = hass.config_entries.async_entries(DOMAIN)[0]
|
||||
entries = hass.config_entries.async_loaded_entries(DOMAIN)
|
||||
if not entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="entry_not_loaded",
|
||||
)
|
||||
entry: GoogleAssistantSDKConfigEntry = entries[0]
|
||||
|
||||
session = entry.runtime_data.session
|
||||
try:
|
||||
|
@@ -1,4 +1,4 @@
|
||||
"""Support for Google Assistant SDK."""
|
||||
"""Services for the Google Assistant SDK integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
@@ -65,6 +65,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded"
|
||||
},
|
||||
"grpc_error": {
|
||||
"message": "Failed to communicate with Google Assistant"
|
||||
}
|
||||
|
@@ -456,6 +456,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
"""Initialize the agent."""
|
||||
self.entry = entry
|
||||
self.subentry = subentry
|
||||
self.default_model = default_model
|
||||
self._attr_name = subentry.title
|
||||
self._genai_client = entry.runtime_data
|
||||
self._attr_unique_id = subentry.subentry_id
|
||||
@@ -489,7 +490,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
tools = tools or []
|
||||
tools.append(Tool(google_search=GoogleSearch()))
|
||||
|
||||
model_name = options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL)
|
||||
model_name = options.get(CONF_CHAT_MODEL, self.default_model)
|
||||
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
|
||||
supports_system_instruction = (
|
||||
"gemma" not in model_name
|
||||
@@ -620,6 +621,13 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
def create_generate_content_config(self) -> GenerateContentConfig:
|
||||
"""Create the GenerateContentConfig for the LLM."""
|
||||
options = self.subentry.data
|
||||
model = options.get(CONF_CHAT_MODEL, self.default_model)
|
||||
thinking_config: ThinkingConfig | None = None
|
||||
if model.startswith("models/gemini-2.5") and not model.endswith(
|
||||
("tts", "image", "image-preview")
|
||||
):
|
||||
thinking_config = ThinkingConfig(include_thoughts=True)
|
||||
|
||||
return GenerateContentConfig(
|
||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||
top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
||||
@@ -652,7 +660,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
||||
),
|
||||
),
|
||||
],
|
||||
thinking_config=ThinkingConfig(include_thoughts=True),
|
||||
thinking_config=thinking_config,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["aiohasupervisor==0.3.3b0"],
|
||||
"requirements": ["aiohasupervisor==0.3.3"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.81", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.82", "babel==2.15.0"]
|
||||
}
|
||||
|
@@ -67,11 +67,7 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant Connect ZBT-2 firmware methods."""
|
||||
|
||||
context: ConfigFlowContext
|
||||
|
||||
# `rts_dtr` targets older adapters, `baudrate` works for newer ones. The reason we
|
||||
# try them in this order is that on older adapters `baudrate` entered the ESP32-S3
|
||||
# bootloader instead of the MG24 bootloader.
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
@@ -157,7 +157,7 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Connect ZBT-2 firmware update entity."""
|
||||
|
||||
bootloader_reset_methods = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
||||
bootloader_reset_methods = [ResetTarget.RTS_DTR]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@@ -1,15 +1,20 @@
|
||||
"""Home Assistant Hardware integration helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import Protocol
|
||||
from typing import TYPE_CHECKING, Protocol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||
|
||||
from . import DATA_COMPONENT
|
||||
from .util import FirmwareInfo
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .util import FirmwareInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +56,7 @@ class HardwareInfoDispatcher:
|
||||
self._notification_callbacks: defaultdict[
|
||||
str, set[Callable[[FirmwareInfo], None]]
|
||||
] = defaultdict(set)
|
||||
self._active_firmware_updates: dict[str, str] = {}
|
||||
|
||||
def register_firmware_info_provider(
|
||||
self, domain: str, platform: HardwareFirmwareInfoModule
|
||||
@@ -118,6 +124,36 @@ class HardwareInfoDispatcher:
|
||||
if fw_info is not None:
|
||||
yield fw_info
|
||||
|
||||
def register_firmware_update_in_progress(
|
||||
self, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Register that a firmware update is in progress for a device."""
|
||||
if device in self._active_firmware_updates:
|
||||
current_domain = self._active_firmware_updates[device]
|
||||
raise ValueError(
|
||||
f"Firmware update already in progress for {device} by {current_domain}"
|
||||
)
|
||||
self._active_firmware_updates[device] = source_domain
|
||||
|
||||
def unregister_firmware_update_in_progress(
|
||||
self, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Unregister a firmware update for a device."""
|
||||
if device not in self._active_firmware_updates:
|
||||
raise ValueError(f"No firmware update in progress for {device}")
|
||||
|
||||
if self._active_firmware_updates[device] != source_domain:
|
||||
current_domain = self._active_firmware_updates[device]
|
||||
raise ValueError(
|
||||
f"Firmware update for {device} is owned by {current_domain}, not {source_domain}"
|
||||
)
|
||||
|
||||
del self._active_firmware_updates[device]
|
||||
|
||||
def is_firmware_update_in_progress(self, device: str) -> bool:
|
||||
"""Check if a firmware update is in progress for a device."""
|
||||
return device in self._active_firmware_updates
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_firmware_info_provider(
|
||||
@@ -141,3 +177,42 @@ def async_notify_firmware_info(
|
||||
) -> Awaitable[None]:
|
||||
"""Notify the dispatcher of new firmware information."""
|
||||
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_register_firmware_update_in_progress(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Register that a firmware update is in progress for a device."""
|
||||
return hass.data[DATA_COMPONENT].register_firmware_update_in_progress(
|
||||
device, source_domain
|
||||
)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_unregister_firmware_update_in_progress(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> None:
|
||||
"""Unregister a firmware update for a device."""
|
||||
return hass.data[DATA_COMPONENT].unregister_firmware_update_in_progress(
|
||||
device, source_domain
|
||||
)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def async_is_firmware_update_in_progress(hass: HomeAssistant, device: str) -> bool:
|
||||
"""Check if a firmware update is in progress for a device."""
|
||||
return hass.data[DATA_COMPONENT].is_firmware_update_in_progress(device)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def async_firmware_update_context(
|
||||
hass: HomeAssistant, device: str, source_domain: str
|
||||
) -> AsyncIterator[None]:
|
||||
"""Register a device as having its firmware being actively updated."""
|
||||
async_register_firmware_update_in_progress(hass, device, source_domain)
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
async_unregister_firmware_update_in_progress(hass, device, source_domain)
|
||||
|
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"universal-silabs-flasher==0.0.34",
|
||||
"universal-silabs-flasher==0.0.35",
|
||||
"ha-silabs-firmware-client==0.2.0"
|
||||
]
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a 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.",
|
||||
"not_hassio_thread": "The OpenThread Border Router add-on can only be installed with Home Assistant OS. If you would like to use the {model} as a Thread border router, please manually 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.",
|
||||
|
@@ -275,6 +275,7 @@ class BaseFirmwareUpdateEntity(
|
||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||
bootloader_reset_methods=self.bootloader_reset_methods,
|
||||
progress_callback=self._update_progress,
|
||||
domain=self._config_entry.domain,
|
||||
)
|
||||
finally:
|
||||
self._attr_in_progress = False
|
||||
|
@@ -26,6 +26,7 @@ from homeassistant.helpers.singleton import singleton
|
||||
|
||||
from . import DATA_COMPONENT
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
OTBR_ADDON_MANAGER_DATA,
|
||||
OTBR_ADDON_NAME,
|
||||
OTBR_ADDON_SLUG,
|
||||
@@ -33,6 +34,7 @@ from .const import (
|
||||
ZIGBEE_FLASHER_ADDON_NAME,
|
||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||
)
|
||||
from .helpers import async_firmware_update_context
|
||||
from .silabs_multiprotocol_addon import (
|
||||
WaitingAddonManager,
|
||||
get_multiprotocol_addon_manager,
|
||||
@@ -359,45 +361,50 @@ async def async_flash_silabs_firmware(
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
*,
|
||||
domain: str = DOMAIN,
|
||||
) -> FirmwareInfo:
|
||||
"""Flash firmware to the SiLabs device."""
|
||||
firmware_info = await guess_firmware_info(hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
async with async_firmware_update_context(hass, device, domain):
|
||||
firmware_info = await guess_firmware_info(hass, device)
|
||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
||||
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(fw_image, progress_callback=progress_callback)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(expected_installed_firmware_type,),
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=(
|
||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||
ApplicationType.EZSP.as_flasher_application_type(),
|
||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||
ApplicationType.CPC.as_flasher_application_type(),
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
async with AsyncExitStack() as stack:
|
||||
for owner in firmware_info.owners:
|
||||
await stack.enter_async_context(owner.temporarily_stop(hass))
|
||||
|
||||
return probed_firmware_info
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
await flasher.enter_bootloader()
|
||||
|
||||
# Flash the firmware, with progress
|
||||
await flasher.flash_firmware(
|
||||
fw_image, progress_callback=progress_callback
|
||||
)
|
||||
except Exception as err:
|
||||
raise HomeAssistantError("Failed to flash firmware") from err
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
probe_methods=(expected_installed_firmware_type,),
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
||||
|
||||
return probed_firmware_info
|
||||
|
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.18"],
|
||||
"requirements": ["aiohomekit==3.2.19"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
@@ -8,13 +8,16 @@ from idasen_ha import Desk
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
|
||||
|
||||
UPDATE_DEBOUNCE_TIME = 0.2
|
||||
|
||||
|
||||
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||
"""Class to manage updates for the Idasen Desk."""
|
||||
@@ -33,9 +36,22 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||
hass, _LOGGER, config_entry=config_entry, name=config_entry.title
|
||||
)
|
||||
self.address = address
|
||||
self._expected_connected = False
|
||||
self.desk = Desk(self._async_handle_update)
|
||||
|
||||
self.desk = Desk(self.async_set_updated_data)
|
||||
self._expected_connected = False
|
||||
self._height: int | None = None
|
||||
|
||||
@callback
|
||||
def async_update_data() -> None:
|
||||
self.async_set_updated_data(self._height)
|
||||
|
||||
self._debouncer = Debouncer(
|
||||
hass=self.hass,
|
||||
logger=_LOGGER,
|
||||
cooldown=UPDATE_DEBOUNCE_TIME,
|
||||
immediate=True,
|
||||
function=async_update_data,
|
||||
)
|
||||
|
||||
async def async_connect(self) -> bool:
|
||||
"""Connect to desk."""
|
||||
@@ -60,3 +76,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||
"""Ensure that the desk is connected if that is the expected state."""
|
||||
if self._expected_connected:
|
||||
await self.async_connect()
|
||||
|
||||
@callback
|
||||
def _async_handle_update(self, height: int | None) -> None:
|
||||
"""Handle an update from the desk."""
|
||||
self._height = height
|
||||
self._debouncer.async_schedule_call()
|
||||
|
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"domain": "iometer",
|
||||
"name": "IOmeter",
|
||||
"codeowners": ["@MaestroOnICe"],
|
||||
"codeowners": ["@jukrebs"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/iometer",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["iometer==0.1.0"],
|
||||
"requirements": ["iometer==0.2.0"],
|
||||
"zeroconf": ["_iometer._tcp.local."]
|
||||
}
|
||||
|
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.1.1"]
|
||||
"requirements": ["pylamarzocco==2.1.2"]
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.translation import async_get_cached_translations
|
||||
|
||||
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
|
||||
|
||||
@@ -62,12 +63,15 @@ class MediaSourceItem:
|
||||
async def async_browse(self) -> BrowseMediaSource:
|
||||
"""Browse this item."""
|
||||
if self.domain is None:
|
||||
title = async_get_cached_translations(
|
||||
self.hass, self.hass.config.language, "common", "media_source"
|
||||
).get("component.media_source.common.sources_default", "Media Sources")
|
||||
base = BrowseMediaSource(
|
||||
domain=None,
|
||||
identifier=None,
|
||||
media_class=MediaClass.APP,
|
||||
media_content_type=MediaType.APPS,
|
||||
title="Media Sources",
|
||||
title=title,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.APP,
|
||||
|
@@ -9,5 +9,8 @@
|
||||
"unknown_media_source": {
|
||||
"message": "Unknown media source: {domain}"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"sources_default": "Media sources"
|
||||
}
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pymelcloud"],
|
||||
"requirements": ["python-melcloud==0.1.0"]
|
||||
"requirements": ["python-melcloud==0.1.2"]
|
||||
}
|
||||
|
@@ -54,6 +54,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_PLATE_COUNT = 4
|
||||
|
||||
PLATE_COUNT = {
|
||||
"KM7575": 6,
|
||||
"KM7678": 6,
|
||||
"KM7697": 6,
|
||||
"KM7878": 6,
|
||||
|
@@ -208,7 +208,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
||||
|
||||
def __process_raw_value(self, entry: float | str | bytes) -> str | None:
|
||||
"""Process value from sensor with NaN handling, scaling, offset, min/max etc."""
|
||||
if self._nan_value and entry in (self._nan_value, -self._nan_value):
|
||||
if self._nan_value is not None and entry in (self._nan_value, -self._nan_value):
|
||||
return None
|
||||
if isinstance(entry, bytes):
|
||||
return entry.decode()
|
||||
|
@@ -253,6 +253,7 @@ class ModbusHub:
|
||||
self._client: (
|
||||
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
||||
) = None
|
||||
self._lock = asyncio.Lock()
|
||||
self.event_connected = asyncio.Event()
|
||||
self.hass = hass
|
||||
self.name = client_config[CONF_NAME]
|
||||
@@ -415,7 +416,9 @@ class ModbusHub:
|
||||
"""Convert async to sync pymodbus call."""
|
||||
if not self._client:
|
||||
return None
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
async with self._lock:
|
||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||
if self._msg_wait:
|
||||
# small delay until next request/response
|
||||
await asyncio.sleep(self._msg_wait)
|
||||
return result
|
||||
|
@@ -188,7 +188,10 @@ class MqttLock(MqttEntity, LockEntity):
|
||||
return
|
||||
if payload == self._config[CONF_PAYLOAD_RESET]:
|
||||
# Reset the state to `unknown`
|
||||
self._attr_is_locked = None
|
||||
self._attr_is_locked = self._attr_is_locking = None
|
||||
self._attr_is_unlocking = None
|
||||
self._attr_is_open = self._attr_is_opening = None
|
||||
self._attr_is_jammed = None
|
||||
elif payload in self._valid_states:
|
||||
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
||||
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
||||
|
@@ -34,6 +34,7 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = NordPoolDataUpdateCoordinator(hass, config_entry)
|
||||
await coordinator.fetch_data(dt_util.utcnow(), True)
|
||||
await coordinator.update_listeners(dt_util.utcnow())
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
|
@@ -44,9 +44,10 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
|
||||
name=DOMAIN,
|
||||
)
|
||||
self.client = NordPoolClient(session=async_get_clientsession(hass))
|
||||
self.unsub: Callable[[], None] | None = None
|
||||
self.data_unsub: Callable[[], None] | None = None
|
||||
self.listener_unsub: Callable[[], None] | None = None
|
||||
|
||||
def get_next_interval(self, now: datetime) -> datetime:
|
||||
def get_next_data_interval(self, now: datetime) -> datetime:
|
||||
"""Compute next time an update should occur."""
|
||||
next_hour = dt_util.utcnow() + timedelta(hours=1)
|
||||
next_run = datetime(
|
||||
@@ -56,23 +57,45 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
|
||||
next_hour.hour,
|
||||
tzinfo=dt_util.UTC,
|
||||
)
|
||||
LOGGER.debug("Next update at %s", next_run)
|
||||
LOGGER.debug("Next data update at %s", next_run)
|
||||
return next_run
|
||||
|
||||
def get_next_15_interval(self, now: datetime) -> datetime:
|
||||
"""Compute next time we need to notify listeners."""
|
||||
next_run = dt_util.utcnow() + timedelta(minutes=15)
|
||||
next_minute = next_run.minute // 15 * 15
|
||||
next_run = next_run.replace(
|
||||
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
|
||||
)
|
||||
|
||||
LOGGER.debug("Next listener update at %s", next_run)
|
||||
return next_run
|
||||
|
||||
async def async_shutdown(self) -> None:
|
||||
"""Cancel any scheduled call, and ignore new runs."""
|
||||
await super().async_shutdown()
|
||||
if self.unsub:
|
||||
self.unsub()
|
||||
self.unsub = None
|
||||
if self.data_unsub:
|
||||
self.data_unsub()
|
||||
self.data_unsub = None
|
||||
if self.listener_unsub:
|
||||
self.listener_unsub()
|
||||
self.listener_unsub = None
|
||||
|
||||
async def update_listeners(self, now: datetime) -> None:
|
||||
"""Update entity listeners."""
|
||||
self.listener_unsub = async_track_point_in_utc_time(
|
||||
self.hass,
|
||||
self.update_listeners,
|
||||
self.get_next_15_interval(dt_util.utcnow()),
|
||||
)
|
||||
self.async_update_listeners()
|
||||
|
||||
async def fetch_data(self, now: datetime, initial: bool = False) -> None:
|
||||
"""Fetch data from Nord Pool."""
|
||||
self.unsub = async_track_point_in_utc_time(
|
||||
self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow())
|
||||
self.data_unsub = async_track_point_in_utc_time(
|
||||
self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow())
|
||||
)
|
||||
if self.config_entry.pref_disable_polling and not initial:
|
||||
self.async_update_listeners()
|
||||
return
|
||||
try:
|
||||
data = await self.handle_data(initial)
|
||||
|
@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
) from error
|
||||
except NordPoolEmptyResponseError:
|
||||
return {area: [] for area in areas}
|
||||
except NordPoolError as error:
|
||||
except (NordPoolError, TimeoutError) as error:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="connection_error",
|
||||
|
@@ -307,7 +307,7 @@
|
||||
},
|
||||
"markdown": {
|
||||
"name": "Format as Markdown",
|
||||
"description": "Enable Markdown formatting for the message body (Web app only). See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/."
|
||||
"description": "Enable Markdown formatting for the message body. See the Markdown guide for syntax details: https://www.markdownguide.org/basic-syntax/."
|
||||
},
|
||||
"tags": {
|
||||
"name": "Tags/Emojis",
|
||||
|
@@ -35,7 +35,7 @@ from .const import CONF_DELETE_PERMANENTLY, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
||||
from .coordinator import OneDriveConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
UPLOAD_CHUNK_SIZE = 16 * 320 * 1024 # 5.2MB
|
||||
UPLOAD_CHUNK_SIZE = 32 * 320 * 1024 # 10.4MB
|
||||
TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours
|
||||
METADATA_VERSION = 2
|
||||
CACHE_TTL = 300
|
||||
@@ -163,7 +163,10 @@ class OneDriveBackupAgent(BackupAgent):
|
||||
)
|
||||
try:
|
||||
backup_file = await LargeFileUploadClient.upload(
|
||||
self._token_function, file, session=async_get_clientsession(self._hass)
|
||||
self._token_function,
|
||||
file,
|
||||
upload_chunk_size=UPLOAD_CHUNK_SIZE,
|
||||
session=async_get_clientsession(self._hass),
|
||||
)
|
||||
except HashMismatchError as err:
|
||||
raise BackupAgentError(
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.15.5"]
|
||||
"requirements": ["opower==0.15.6"]
|
||||
}
|
||||
|
@@ -75,6 +75,9 @@ async def _title(hass: HomeAssistant, discovery_info: HassioServiceInfo) -> str:
|
||||
if device and ("Connect_ZBT-1" in device or "SkyConnect" in device):
|
||||
return f"Home Assistant Connect ZBT-1 ({discovery_info.name})"
|
||||
|
||||
if device and "Nabu_Casa_ZBT-2" in device:
|
||||
return f"Home Assistant Connect ZBT-2 ({discovery_info.name})"
|
||||
|
||||
return discovery_info.name
|
||||
|
||||
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ovoenergy"],
|
||||
"requirements": ["ovoenergy==2.0.1"]
|
||||
"requirements": ["ovoenergy==3.0.1"]
|
||||
}
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/portainer",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==0.1.7"]
|
||||
"requirements": ["pyportainer==1.0.3"]
|
||||
}
|
||||
|
@@ -215,6 +215,7 @@ def create_coordinator_container_vm(
|
||||
return DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=None,
|
||||
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
|
||||
update_method=async_update_data,
|
||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||
|
@@ -16,7 +16,6 @@ ATTR_HTML: Final = "html"
|
||||
ATTR_CALLBACK_URL: Final = "callback_url"
|
||||
ATTR_EXPIRE: Final = "expire"
|
||||
ATTR_TTL: Final = "ttl"
|
||||
ATTR_DATA: Final = "data"
|
||||
ATTR_TIMESTAMP: Final = "timestamp"
|
||||
|
||||
CONF_USER_KEY: Final = "user_key"
|
||||
|
@@ -67,7 +67,7 @@ class PushoverNotificationService(BaseNotificationService):
|
||||
|
||||
# Extract params from data dict
|
||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||
data = kwargs.get(ATTR_DATA, {})
|
||||
data = kwargs.get(ATTR_DATA) or {}
|
||||
url = data.get(ATTR_URL)
|
||||
url_title = data.get(ATTR_URL_TITLE)
|
||||
priority = data.get(ATTR_PRIORITY)
|
||||
|
@@ -39,6 +39,23 @@ from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def _get_filtered_vehicles(account: RenaultAccount) -> list[KamereonVehiclesLink]:
|
||||
"""Filter out vehicles with missing details.
|
||||
|
||||
May be due to new purchases, or issue with the Renault servers.
|
||||
"""
|
||||
vehicles = await account.get_vehicles()
|
||||
if not vehicles.vehicleLinks:
|
||||
return []
|
||||
result: list[KamereonVehiclesLink] = []
|
||||
for link in vehicles.vehicleLinks:
|
||||
if link.vehicleDetails is None:
|
||||
LOGGER.warning("Ignoring vehicle with missing details: %s", link.vin)
|
||||
continue
|
||||
result.append(link)
|
||||
return result
|
||||
|
||||
|
||||
class RenaultHub:
|
||||
"""Handle account communication with Renault servers."""
|
||||
|
||||
@@ -84,49 +101,48 @@ class RenaultHub:
|
||||
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
||||
|
||||
self._account = await self._client.get_api_account(account_id)
|
||||
vehicles = await self._account.get_vehicles()
|
||||
if vehicles.vehicleLinks:
|
||||
if any(
|
||||
vehicle_link.vehicleDetails is None
|
||||
for vehicle_link in vehicles.vehicleLinks
|
||||
):
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to retrieve vehicle details from Renault servers"
|
||||
)
|
||||
|
||||
num_call_per_scan = len(COORDINATORS) * len(vehicles.vehicleLinks)
|
||||
scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
vehicle_links = await _get_filtered_vehicles(self._account)
|
||||
if not vehicle_links:
|
||||
LOGGER.debug(
|
||||
"No valid vehicle details found for account_id: %s", account_id
|
||||
)
|
||||
raise ConfigEntryNotReady(
|
||||
"Failed to retrieve vehicle details from Renault servers"
|
||||
)
|
||||
|
||||
device_registry = dr.async_get(self._hass)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_initialise_vehicle(
|
||||
vehicle_link,
|
||||
self._account,
|
||||
scan_interval,
|
||||
config_entry,
|
||||
device_registry,
|
||||
)
|
||||
for vehicle_link in vehicles.vehicleLinks
|
||||
)
|
||||
)
|
||||
num_call_per_scan = len(COORDINATORS) * len(vehicle_links)
|
||||
scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
|
||||
# all vehicles have been initiated with the right number of active coordinators
|
||||
num_call_per_scan = 0
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
device_registry = dr.async_get(self._hass)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
self.async_initialise_vehicle(
|
||||
vehicle_link,
|
||||
self._account,
|
||||
scan_interval,
|
||||
config_entry,
|
||||
device_registry,
|
||||
)
|
||||
for vehicle_link in vehicle_links
|
||||
)
|
||||
)
|
||||
|
||||
# all vehicles have been initiated with the right number of active coordinators
|
||||
num_call_per_scan = 0
|
||||
for vehicle_link in vehicle_links:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
num_call_per_scan += len(vehicle.coordinators)
|
||||
|
||||
new_scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
if new_scan_interval != scan_interval:
|
||||
# we need to change the vehicles with the right scan interval
|
||||
for vehicle_link in vehicle_links:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
num_call_per_scan += len(vehicle.coordinators)
|
||||
|
||||
new_scan_interval = timedelta(
|
||||
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||
)
|
||||
if new_scan_interval != scan_interval:
|
||||
# we need to change the vehicles with the right scan interval
|
||||
for vehicle_link in vehicles.vehicleLinks:
|
||||
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||
vehicle.update_scan_interval(new_scan_interval)
|
||||
vehicle.update_scan_interval(new_scan_interval)
|
||||
|
||||
async def async_initialise_vehicle(
|
||||
self,
|
||||
@@ -164,10 +180,10 @@ class RenaultHub:
|
||||
"""Get Kamereon account ids."""
|
||||
accounts = []
|
||||
for account in await self._client.get_api_accounts():
|
||||
vehicles = await account.get_vehicles()
|
||||
vehicle_links = await _get_filtered_vehicles(account)
|
||||
|
||||
# Only add the account if it has linked vehicles.
|
||||
if vehicles.vehicleLinks:
|
||||
if vehicle_links:
|
||||
accounts.append(account.account_id)
|
||||
return accounts
|
||||
|
||||
|
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.16.0"]
|
||||
"requirements": ["reolink-aio==0.16.1"]
|
||||
}
|
||||
|
@@ -82,7 +82,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
assert self._client
|
||||
errors: dict[str, str] = {}
|
||||
try:
|
||||
await self._client.request_code()
|
||||
await self._client.request_code_v4()
|
||||
except RoborockAccountDoesNotExist:
|
||||
errors["base"] = "invalid_email"
|
||||
except RoborockUrlException:
|
||||
@@ -111,7 +111,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
code = user_input[CONF_ENTRY_CODE]
|
||||
_LOGGER.debug("Logging into Roborock account using email provided code")
|
||||
try:
|
||||
user_data = await self._client.code_login(code)
|
||||
user_data = await self._client.code_login_v4(code)
|
||||
except RoborockInvalidCode:
|
||||
errors["base"] = "invalid_code"
|
||||
except RoborockException:
|
||||
@@ -129,7 +129,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()}
|
||||
)
|
||||
self._abort_if_unique_id_configured(error="already_configured_account")
|
||||
return self._create_entry(self._client, self._username, user_data)
|
||||
return await self._create_entry(self._client, self._username, user_data)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="code",
|
||||
@@ -176,7 +176,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_code()
|
||||
return self.async_show_form(step_id="reauth_confirm", errors=errors)
|
||||
|
||||
def _create_entry(
|
||||
async def _create_entry(
|
||||
self, client: RoborockApiClient, username: str, user_data: UserData
|
||||
) -> ConfigFlowResult:
|
||||
"""Finished config flow and create entry."""
|
||||
@@ -185,7 +185,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_USER_DATA: user_data.as_dict(),
|
||||
CONF_BASE_URL: client.base_url,
|
||||
CONF_BASE_URL: await client.base_url,
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -19,7 +19,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==2.47.1",
|
||||
"python-roborock==2.50.2",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
@@ -377,8 +377,10 @@
|
||||
"max": "Max",
|
||||
"high": "[%key:common::state::high%]",
|
||||
"intense": "Intense",
|
||||
"extreme": "Extreme",
|
||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||
"custom_water_flow": "Custom water flow",
|
||||
"vac_followed_by_mop": "Vacuum followed by mop",
|
||||
"smart_mode": "[%key:component::roborock::entity::select::mop_mode::state::smart_mode%]"
|
||||
}
|
||||
},
|
||||
|
@@ -13,8 +13,10 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
CONF_OUTPUT_NUMBER,
|
||||
CONF_OUTPUTS,
|
||||
CONF_ZONE_NUMBER,
|
||||
CONF_ZONE_TYPE,
|
||||
CONF_ZONES,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
SUBENTRY_TYPE_OUTPUT,
|
||||
@@ -49,7 +51,7 @@ async def async_setup_entry(
|
||||
zone_num,
|
||||
zone_name,
|
||||
zone_type,
|
||||
SUBENTRY_TYPE_ZONE,
|
||||
CONF_ZONES,
|
||||
SIGNAL_ZONES_UPDATED,
|
||||
)
|
||||
],
|
||||
@@ -73,7 +75,7 @@ async def async_setup_entry(
|
||||
output_num,
|
||||
output_name,
|
||||
ouput_type,
|
||||
SUBENTRY_TYPE_OUTPUT,
|
||||
CONF_OUTPUTS,
|
||||
SIGNAL_OUTPUTS_UPDATED,
|
||||
)
|
||||
],
|
||||
|
@@ -24,6 +24,7 @@
|
||||
},
|
||||
"config_subentries": {
|
||||
"partition": {
|
||||
"entry_type": "Partition",
|
||||
"initiate_flow": {
|
||||
"user": "Add partition"
|
||||
},
|
||||
@@ -57,6 +58,7 @@
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"entry_type": "Zone",
|
||||
"initiate_flow": {
|
||||
"user": "Add zone"
|
||||
},
|
||||
@@ -91,6 +93,7 @@
|
||||
}
|
||||
},
|
||||
"output": {
|
||||
"entry_type": "Output",
|
||||
"initiate_flow": {
|
||||
"user": "Add output"
|
||||
},
|
||||
@@ -125,6 +128,7 @@
|
||||
}
|
||||
},
|
||||
"switchable_output": {
|
||||
"entry_type": "Switchable output",
|
||||
"initiate_flow": {
|
||||
"user": "Add switchable output"
|
||||
},
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sharkiq"],
|
||||
"requirements": ["sharkiq==1.4.0"]
|
||||
"requirements": ["sharkiq==1.4.2"]
|
||||
}
|
||||
|
@@ -319,7 +319,7 @@ RPC_SENSORS: Final = {
|
||||
),
|
||||
"presencezone_state": RpcBinarySensorDescription(
|
||||
key="presencezone",
|
||||
sub_key="state",
|
||||
sub_key="value",
|
||||
name="Occupancy",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
entity_class=RpcPresenceBinarySensor,
|
||||
|
@@ -226,6 +226,8 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
|
||||
def _update_callback(self) -> None:
|
||||
"""Handle device update. Use a task when opening/closing is in progress."""
|
||||
super()._update_callback()
|
||||
if not self.coordinator.device.initialized:
|
||||
return
|
||||
if self.is_closing or self.is_opening:
|
||||
self.launch_update_task()
|
||||
|
||||
|
@@ -692,27 +692,25 @@ def async_remove_orphaned_entities(
|
||||
"""Remove orphaned entities."""
|
||||
orphaned_entities = []
|
||||
entity_reg = er.async_get(hass)
|
||||
device_reg = dr.async_get(hass)
|
||||
|
||||
if not (
|
||||
devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id)
|
||||
):
|
||||
return
|
||||
entities = er.async_entries_for_config_entry(entity_reg, config_entry_id)
|
||||
for entity in entities:
|
||||
if not entity.entity_id.startswith(platform):
|
||||
continue
|
||||
if key_suffix is not None and key_suffix not in entity.unique_id:
|
||||
continue
|
||||
# we are looking for the component ID, e.g. boolean:201, em1data:1
|
||||
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
|
||||
continue
|
||||
|
||||
for device in devices:
|
||||
entities = er.async_entries_for_device(entity_reg, device.id, True)
|
||||
for entity in entities:
|
||||
if not entity.entity_id.startswith(platform):
|
||||
continue
|
||||
if key_suffix is not None and key_suffix not in entity.unique_id:
|
||||
continue
|
||||
# we are looking for the component ID, e.g. boolean:201, em1data:1
|
||||
if not (match := COMPONENT_ID_PATTERN.search(entity.unique_id)):
|
||||
continue
|
||||
|
||||
key = match.group()
|
||||
if key not in keys:
|
||||
orphaned_entities.append(entity.unique_id.split("-", 1)[1])
|
||||
key = match.group()
|
||||
if key not in keys:
|
||||
LOGGER.debug(
|
||||
"Found orphaned Shelly entity: %s, unique id: %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
)
|
||||
orphaned_entities.append(entity.unique_id.split("-", 1)[1])
|
||||
|
||||
if orphaned_entities:
|
||||
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
|
||||
|
@@ -100,8 +100,9 @@ ATTR_PIN_VALUE = "pin"
|
||||
ATTR_TIMESTAMP = "timestamp"
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
||||
DEFAULT_SOCKET_MIN_RETRY = 15
|
||||
|
||||
WEBSOCKET_RECONNECT_RETRIES = 3
|
||||
WEBSOCKET_RETRY_DELAY = 2
|
||||
|
||||
EVENT_SIMPLISAFE_EVENT = "SIMPLISAFE_EVENT"
|
||||
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
||||
@@ -419,6 +420,7 @@ class SimpliSafe:
|
||||
self._api = api
|
||||
self._hass = hass
|
||||
self._system_notifications: dict[int, set[SystemNotification]] = {}
|
||||
self._websocket_reconnect_retries: int = 0
|
||||
self._websocket_reconnect_task: asyncio.Task | None = None
|
||||
self.entry = entry
|
||||
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
||||
@@ -469,6 +471,8 @@ class SimpliSafe:
|
||||
"""Start a websocket reconnection loop."""
|
||||
assert self._api.websocket
|
||||
|
||||
self._websocket_reconnect_retries += 1
|
||||
|
||||
try:
|
||||
await self._api.websocket.async_connect()
|
||||
await self._api.websocket.async_listen()
|
||||
@@ -479,9 +483,21 @@ class SimpliSafe:
|
||||
LOGGER.error("Failed to connect to websocket: %s", err)
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
|
||||
else:
|
||||
self._websocket_reconnect_retries = 0
|
||||
|
||||
LOGGER.debug("Reconnecting to websocket")
|
||||
await self._async_cancel_websocket_loop()
|
||||
if self._websocket_reconnect_retries >= WEBSOCKET_RECONNECT_RETRIES:
|
||||
LOGGER.error("Max websocket connection retries exceeded")
|
||||
return
|
||||
|
||||
delay = WEBSOCKET_RETRY_DELAY * (2 ** (self._websocket_reconnect_retries - 1))
|
||||
LOGGER.info(
|
||||
"Retrying websocket connection in %s seconds (attempt %s/%s)",
|
||||
delay,
|
||||
self._websocket_reconnect_retries,
|
||||
WEBSOCKET_RECONNECT_RETRIES,
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
self._websocket_reconnect_task = self._hass.async_create_task(
|
||||
self._async_start_websocket_loop()
|
||||
)
|
||||
|
@@ -5,14 +5,14 @@
|
||||
"description": "Refer to the documentation on getting your Slack API key.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"default_channel": "Default Channel",
|
||||
"default_channel": "Default channel",
|
||||
"icon": "Icon",
|
||||
"username": "[%key:common::config_flow::data::username%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "The Slack API token to use for sending Slack messages.",
|
||||
"default_channel": "The channel to post to if no channel is specified when sending a message.",
|
||||
"icon": "Use one of the Slack emojis as an Icon for the supplied username.",
|
||||
"icon": "Use one of the Slack emojis as an icon for the supplied username.",
|
||||
"username": "Home Assistant will post to Slack using the username specified."
|
||||
}
|
||||
}
|
||||
|
@@ -109,6 +109,8 @@ PRESET_MODE_TO_HA = {
|
||||
"quiet": "quiet",
|
||||
"longWind": "long_wind",
|
||||
"smart": "smart",
|
||||
"motionIndirect": "motion_indirect",
|
||||
"motionDirect": "motion_direct",
|
||||
}
|
||||
|
||||
HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()}
|
||||
|
@@ -31,6 +31,17 @@
|
||||
"default": "mdi:stop"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"air_conditioner": {
|
||||
"state_attributes": {
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"turbo": "mdi:wind-power"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
"washer_rinse_cycles": {
|
||||
"default": "mdi:waves-arrow-up"
|
||||
|
@@ -30,5 +30,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.3.0"]
|
||||
"requirements": ["pysmartthings==3.3.1"]
|
||||
}
|
||||
|
@@ -1151,8 +1151,11 @@ async def async_setup_entry(
|
||||
)
|
||||
and (
|
||||
not description.exists_fn
|
||||
or description.exists_fn(
|
||||
device.status[MAIN][capability][attribute]
|
||||
or (
|
||||
component == MAIN
|
||||
and description.exists_fn(
|
||||
device.status[MAIN][capability][attribute]
|
||||
)
|
||||
)
|
||||
)
|
||||
and (
|
||||
|
@@ -87,7 +87,14 @@
|
||||
"wind_free_sleep": "WindFree sleep",
|
||||
"quiet": "Quiet",
|
||||
"long_wind": "Long wind",
|
||||
"smart": "Smart"
|
||||
"smart": "Smart",
|
||||
"motion_direct": "Motion direct",
|
||||
"motion_indirect": "Motion indirect"
|
||||
}
|
||||
},
|
||||
"fan_mode": {
|
||||
"state": {
|
||||
"turbo": "Turbo"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -10,6 +10,28 @@
|
||||
"zigbee_type": {
|
||||
"default": "mdi:zigbee"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"disable_led": {
|
||||
"default": "mdi:led-off"
|
||||
},
|
||||
"auto_zigbee_update": {
|
||||
"default": "mdi:autorenew"
|
||||
},
|
||||
"night_mode": {
|
||||
"default": "mdi:lightbulb-night"
|
||||
},
|
||||
"vpn_enabled": {
|
||||
"default": "mdi:shield-lock"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"zigbee_flash_mode": {
|
||||
"default": "mdi:memory-arrow-down"
|
||||
},
|
||||
"reconnect_zigbee_router": {
|
||||
"default": "mdi:connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -51,7 +51,6 @@ SWITCHES: list[SmSwitchEntityDescription] = [
|
||||
SmSwitchEntityDescription(
|
||||
key="auto_zigbee_update",
|
||||
translation_key="auto_zigbee_update",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
setting=Settings.ZB_AUTOUPDATE,
|
||||
entity_registry_enabled_default=False,
|
||||
state_fn=lambda x: x.auto_zigbee,
|
||||
@@ -83,6 +82,7 @@ class SmSwitch(SmEntity, SwitchEntity):
|
||||
coordinator: SmDataUpdateCoordinator
|
||||
entity_description: SmSwitchEntityDescription
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@@ -193,7 +193,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
if player.player_id in entry.runtime_data.known_player_ids:
|
||||
await player.async_update()
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_PLAYER_REDISCOVERED, player.player_id, player.connected
|
||||
hass,
|
||||
SIGNAL_PLAYER_REDISCOVERED + entry.entry_id,
|
||||
player.player_id,
|
||||
player.connected,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Adding new entity: %s", player)
|
||||
@@ -203,7 +206,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
await player_coordinator.async_refresh()
|
||||
entry.runtime_data.known_player_ids.add(player.player_id)
|
||||
async_dispatcher_send(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED, player_coordinator
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, player_coordinator
|
||||
)
|
||||
|
||||
if players := await lms.async_get_players():
|
||||
|
@@ -132,7 +132,9 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -117,7 +117,9 @@ class SqueezeBoxPlayerUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
|
||||
# start listening for restored players
|
||||
self._remove_dispatcher = async_dispatcher_connect(
|
||||
self.hass, SIGNAL_PLAYER_REDISCOVERED, self.rediscovered
|
||||
self.hass,
|
||||
SIGNAL_PLAYER_REDISCOVERED + self.config_entry.entry_id,
|
||||
self.rediscovered,
|
||||
)
|
||||
|
||||
alarm_dict: dict[str, Alarm] = (
|
||||
|
@@ -175,7 +175,9 @@ async def async_setup_entry(
|
||||
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
# Register entity services
|
||||
|
@@ -89,7 +89,9 @@ async def async_setup_entry(
|
||||
async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)])
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(hass, SIGNAL_PLAYER_DISCOVERED, _player_discovered)
|
||||
async_dispatcher_connect(
|
||||
hass, SIGNAL_PLAYER_DISCOVERED + entry.entry_id, _player_discovered
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
@@ -143,6 +143,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self.reauth_conf: Mapping[str, Any] = {}
|
||||
self.reauth_reason: str | None = None
|
||||
self.shares: list[SynoFileSharedFolder] | None = None
|
||||
self.api: SynologyDSM | None = None
|
||||
|
||||
def _show_form(
|
||||
self,
|
||||
@@ -156,6 +157,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
description_placeholders = {}
|
||||
data_schema = None
|
||||
self.api = None
|
||||
|
||||
if step_id == "link":
|
||||
user_input.update(self.discovered_conf)
|
||||
@@ -194,14 +196,21 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
port = DEFAULT_PORT
|
||||
|
||||
session = async_get_clientsession(self.hass, verify_ssl)
|
||||
api = SynologyDSM(
|
||||
session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT
|
||||
)
|
||||
if self.api is None:
|
||||
session = async_get_clientsession(self.hass, verify_ssl)
|
||||
self.api = SynologyDSM(
|
||||
session,
|
||||
host,
|
||||
port,
|
||||
username,
|
||||
password,
|
||||
use_ssl,
|
||||
timeout=DEFAULT_TIMEOUT,
|
||||
)
|
||||
|
||||
errors = {}
|
||||
try:
|
||||
serial = await _login_and_fetch_syno_info(api, otp_code)
|
||||
serial = await _login_and_fetch_syno_info(self.api, otp_code)
|
||||
except SynologyDSMLogin2SARequiredException:
|
||||
return await self.async_step_2sa(user_input)
|
||||
except SynologyDSMLogin2SAFailedException:
|
||||
@@ -221,10 +230,11 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "missing_data"
|
||||
|
||||
if errors:
|
||||
self.api = None
|
||||
return self._show_form(step_id, user_input, errors)
|
||||
|
||||
with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS):
|
||||
self.shares = await api.file.get_shared_folders(only_writable=True)
|
||||
self.shares = await self.api.file.get_shared_folders(only_writable=True)
|
||||
|
||||
if self.shares and not backup_path:
|
||||
return await self.async_step_backup_share(user_input)
|
||||
@@ -239,14 +249,14 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_VERIFY_SSL: verify_ssl,
|
||||
CONF_USERNAME: username,
|
||||
CONF_PASSWORD: password,
|
||||
CONF_MAC: api.network.macs,
|
||||
CONF_MAC: self.api.network.macs,
|
||||
}
|
||||
config_options = {
|
||||
CONF_BACKUP_PATH: backup_path,
|
||||
CONF_BACKUP_SHARE: backup_share,
|
||||
}
|
||||
if otp_code:
|
||||
config_data[CONF_DEVICE_TOKEN] = api.device_token
|
||||
config_data[CONF_DEVICE_TOKEN] = self.api.device_token
|
||||
if user_input.get(CONF_DISKS):
|
||||
config_data[CONF_DISKS] = user_input[CONF_DISKS]
|
||||
if user_input.get(CONF_VOLUMES):
|
||||
|
@@ -336,6 +336,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
||||
key="power_usage",
|
||||
translation_key="power_usage",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
suggested_display_precision=2,
|
||||
icon="mdi:power-plug",
|
||||
@@ -577,7 +578,6 @@ async def async_setup_entry(
|
||||
key=f"gpu_{gpu.id}_power_usage",
|
||||
name=f"{gpu.name} power usage",
|
||||
entity_registry_enabled_default=False,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
value=lambda data, k=index: gpu_power_usage(data, k),
|
||||
|
@@ -372,6 +372,7 @@ class TriggerBinarySensorEntity(TriggerEntity, BinarySensorEntity, RestoreEntity
|
||||
def _set_state(self, state, _=None):
|
||||
"""Set up auto off."""
|
||||
self._attr_is_on = state
|
||||
self._delay_cancel = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
if not state:
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tibber"],
|
||||
"requirements": ["pyTibber==0.32.1"]
|
||||
"requirements": ["pyTibber==0.32.2"]
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
@@ -149,8 +150,9 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
raise DeviceNotFound("Unable to connect to device") from exc
|
||||
|
||||
try:
|
||||
packet_a0 = await client.read(PacketA0Notify)
|
||||
except (BleakError, DecodeError) as exc:
|
||||
async with asyncio.timeout(10):
|
||||
packet_a0 = await client.read(PacketA0Notify)
|
||||
except (BleakError, DecodeError, TimeoutError) as exc:
|
||||
await client.disconnect()
|
||||
raise DeviceFailed(f"Device failed {exc}") from exc
|
||||
|
||||
@@ -215,9 +217,19 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
|
||||
@callback
|
||||
def _async_request_refresh_soon(self) -> None:
|
||||
self.config_entry.async_create_task(
|
||||
self.hass, self.async_request_refresh(), eager_start=False
|
||||
)
|
||||
"""Request a refresh in the near future.
|
||||
|
||||
This way have been called during an update and
|
||||
would be ignored by debounce logic, so we delay
|
||||
it by a slight amount to hopefully let the current
|
||||
update finish first.
|
||||
"""
|
||||
|
||||
async def _delayed_refresh() -> None:
|
||||
await asyncio.sleep(0.5)
|
||||
await self.async_request_refresh()
|
||||
|
||||
self.config_entry.async_create_task(self.hass, _delayed_refresh())
|
||||
|
||||
@callback
|
||||
def _disconnected_callback(self) -> None:
|
||||
|
@@ -300,9 +300,10 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||
self._current_state is not None
|
||||
and (current_state := self.device.status.get(self._current_state))
|
||||
is not None
|
||||
and current_state != "stop"
|
||||
):
|
||||
return self.entity_description.current_state_inverse is not (
|
||||
current_state in (True, "fully_close")
|
||||
current_state in (True, "close", "fully_close")
|
||||
)
|
||||
|
||||
return None
|
||||
|
@@ -100,8 +100,9 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
||||
"""Return the currently set speed."""
|
||||
|
||||
current_level = self.device.state.fan_level
|
||||
|
||||
if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None:
|
||||
if current_level == 0:
|
||||
return 0
|
||||
return ordered_list_item_to_percentage(
|
||||
self.device.fan_levels, current_level
|
||||
)
|
||||
@@ -211,17 +212,17 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
||||
await self.device.turn_on()
|
||||
|
||||
if preset_mode == VS_FAN_MODE_AUTO:
|
||||
success = await self.device.auto_mode()
|
||||
success = await self.device.set_auto_mode()
|
||||
elif preset_mode == VS_FAN_MODE_SLEEP:
|
||||
success = await self.device.sleep_mode()
|
||||
success = await self.device.set_sleep_mode()
|
||||
elif preset_mode == VS_FAN_MODE_ADVANCED_SLEEP:
|
||||
success = await self.device.advanced_sleep_mode()
|
||||
success = await self.device.set_advanced_sleep_mode()
|
||||
elif preset_mode == VS_FAN_MODE_PET:
|
||||
success = await self.device.pet_mode()
|
||||
success = await self.device.set_pet_mode()
|
||||
elif preset_mode == VS_FAN_MODE_TURBO:
|
||||
success = await self.device.turbo_mode()
|
||||
success = await self.device.set_turbo_mode()
|
||||
elif preset_mode == VS_FAN_MODE_NORMAL:
|
||||
success = await self.device.normal_mode()
|
||||
success = await self.device.set_normal_mode()
|
||||
if not success:
|
||||
raise HomeAssistantError(self.device.last_response.message)
|
||||
|
||||
|
@@ -13,5 +13,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyvesync"],
|
||||
"requirements": ["pyvesync==3.0.0"]
|
||||
"requirements": ["pyvesync==3.1.0"]
|
||||
}
|
||||
|
@@ -34,12 +34,13 @@ CONF_HEATING_TYPE = "heating_type"
|
||||
|
||||
DEFAULT_CACHE_DURATION = 60
|
||||
|
||||
VICARE_BAR = "bar"
|
||||
VICARE_CUBIC_METER = "cubicMeter"
|
||||
VICARE_KW = "kilowatt"
|
||||
VICARE_KWH = "kilowattHour"
|
||||
VICARE_PERCENT = "percent"
|
||||
VICARE_W = "watt"
|
||||
VICARE_KW = "kilowatt"
|
||||
VICARE_WH = "wattHour"
|
||||
VICARE_KWH = "kilowattHour"
|
||||
VICARE_CUBIC_METER = "cubicMeter"
|
||||
|
||||
|
||||
class HeatingType(enum.Enum):
|
||||
|
@@ -41,6 +41,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
VICARE_BAR,
|
||||
VICARE_CUBIC_METER,
|
||||
VICARE_KW,
|
||||
VICARE_KWH,
|
||||
@@ -62,20 +63,22 @@ from .utils import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
VICARE_UNIT_TO_DEVICE_CLASS = {
|
||||
VICARE_WH: SensorDeviceClass.ENERGY,
|
||||
VICARE_KWH: SensorDeviceClass.ENERGY,
|
||||
VICARE_W: SensorDeviceClass.POWER,
|
||||
VICARE_KW: SensorDeviceClass.POWER,
|
||||
VICARE_BAR: SensorDeviceClass.PRESSURE,
|
||||
VICARE_CUBIC_METER: SensorDeviceClass.GAS,
|
||||
VICARE_KW: SensorDeviceClass.POWER,
|
||||
VICARE_KWH: SensorDeviceClass.ENERGY,
|
||||
VICARE_WH: SensorDeviceClass.ENERGY,
|
||||
VICARE_W: SensorDeviceClass.POWER,
|
||||
}
|
||||
|
||||
VICARE_UNIT_TO_HA_UNIT = {
|
||||
VICARE_BAR: UnitOfPressure.BAR,
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_PERCENT: PERCENTAGE,
|
||||
VICARE_W: UnitOfPower.WATT,
|
||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
||||
VICARE_WH: UnitOfEnergy.WATT_HOUR,
|
||||
VICARE_KWH: UnitOfEnergy.KILO_WATT_HOUR,
|
||||
VICARE_CUBIC_METER: UnitOfVolume.CUBIC_METERS,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -13,7 +13,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -69,7 +69,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""
|
||||
client = VictronVRMClient(
|
||||
token=api_token,
|
||||
client_session=get_async_client(self.hass),
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
sites = await client.users.list_sites()
|
||||
@@ -86,7 +86,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Validate access to the selected site and return its data."""
|
||||
client = VictronVRMClient(
|
||||
token=api_token,
|
||||
client_session=get_async_client(self.hass),
|
||||
client_session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
site_data = await client.users.get_site(site_id)
|
||||
|
@@ -11,7 +11,7 @@ from victron_vrm.utils import dt_now
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER
|
||||
@@ -26,8 +26,8 @@ class VRMForecastStore:
|
||||
"""Class to hold the forecast data."""
|
||||
|
||||
site_id: int
|
||||
solar: ForecastAggregations
|
||||
consumption: ForecastAggregations
|
||||
solar: ForecastAggregations | None
|
||||
consumption: ForecastAggregations | None
|
||||
|
||||
|
||||
async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore:
|
||||
@@ -75,7 +75,7 @@ class VictronRemoteMonitoringDataUpdateCoordinator(
|
||||
"""Initialize."""
|
||||
self.client = VictronVRMClient(
|
||||
token=config_entry.data[CONF_API_TOKEN],
|
||||
client_session=get_async_client(hass),
|
||||
client_session=async_get_clientsession(hass),
|
||||
)
|
||||
self.site_id = config_entry.data[CONF_SITE_ID]
|
||||
super().__init__(
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["victron-vrm==0.1.7"]
|
||||
"requirements": ["victron-vrm==0.1.8"]
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user