mirror of
https://github.com/home-assistant/core.git
synced 2025-10-20 17:19:52 +00:00
Compare commits
100 Commits
cdce8p-bui
...
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
|
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
|
||||||
/homeassistant/components/intesishome/ @jnimmo
|
/homeassistant/components/intesishome/ @jnimmo
|
||||||
/homeassistant/components/iometer/ @MaestroOnICe
|
/homeassistant/components/iometer/ @jukrebs
|
||||||
/tests/components/iometer/ @MaestroOnICe
|
/tests/components/iometer/ @jukrebs
|
||||||
/homeassistant/components/ios/ @robbiet480
|
/homeassistant/components/ios/ @robbiet480
|
||||||
/tests/components/ios/ @robbiet480
|
/tests/components/ios/ @robbiet480
|
||||||
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
/homeassistant/components/iotawatt/ @gtdiehl @jyavenard
|
||||||
|
10
build.yaml
10
build.yaml
@@ -1,10 +1,10 @@
|
|||||||
image: ghcr.io/home-assistant/{arch}-homeassistant
|
image: ghcr.io/home-assistant/{arch}-homeassistant
|
||||||
build_from:
|
build_from:
|
||||||
aarch64: ghcr.io/home-assistant/aarch64-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.09.3
|
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
|
||||||
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.09.3
|
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
|
||||||
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.09.3
|
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
|
||||||
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.09.3
|
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
|
||||||
codenotary:
|
codenotary:
|
||||||
signer: notary@home-assistant.io
|
signer: notary@home-assistant.io
|
||||||
base_image: 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_OBSERVATION = timedelta(minutes=10)
|
||||||
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
|
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": {
|
"entity": {
|
||||||
"sensor": {
|
"sensor": {
|
||||||
|
"air_quality": {
|
||||||
|
"default": "mdi:air-filter"
|
||||||
|
},
|
||||||
"cloud_ceiling": {
|
"cloud_ceiling": {
|
||||||
"default": "mdi:weather-fog"
|
"default": "mdi:weather-fog"
|
||||||
},
|
},
|
||||||
@@ -34,9 +37,6 @@
|
|||||||
"thunderstorm_probability_night": {
|
"thunderstorm_probability_night": {
|
||||||
"default": "mdi:weather-lightning"
|
"default": "mdi:weather-lightning"
|
||||||
},
|
},
|
||||||
"translation_key": {
|
|
||||||
"default": "mdi:air-filter"
|
|
||||||
},
|
|
||||||
"tree_pollen": {
|
"tree_pollen": {
|
||||||
"default": "mdi:tree-outline"
|
"default": "mdi:tree-outline"
|
||||||
},
|
},
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
"""Airgradient Update platform."""
|
"""Airgradient Update platform."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from airgradient import AirGradientConnectionError
|
||||||
from propcache.api import cached_property
|
from propcache.api import cached_property
|
||||||
|
|
||||||
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
|
||||||
@@ -13,6 +15,7 @@ from .entity import AirGradientEntity
|
|||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
SCAN_INTERVAL = timedelta(hours=1)
|
SCAN_INTERVAL = timedelta(hours=1)
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@@ -31,6 +34,7 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
|||||||
"""Representation of Airgradient Update."""
|
"""Representation of Airgradient Update."""
|
||||||
|
|
||||||
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
_attr_device_class = UpdateDeviceClass.FIRMWARE
|
||||||
|
_server_unreachable_logged = False
|
||||||
|
|
||||||
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
def __init__(self, coordinator: AirGradientCoordinator) -> None:
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
@@ -47,10 +51,27 @@ class AirGradientUpdate(AirGradientEntity, UpdateEntity):
|
|||||||
"""Return the installed version of the entity."""
|
"""Return the installed version of the entity."""
|
||||||
return self.coordinator.data.measures.firmware_version
|
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:
|
async def async_update(self) -> None:
|
||||||
"""Update the entity."""
|
"""Update the entity."""
|
||||||
self._attr_latest_version = (
|
try:
|
||||||
await self.coordinator.client.get_latest_firmware_version(
|
self._attr_latest_version = (
|
||||||
self.coordinator.serial_number
|
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",
|
"documentation": "https://www.home-assistant.io/integrations/airos",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["airos==0.5.1"]
|
"requirements": ["airos==0.5.5"]
|
||||||
}
|
}
|
||||||
|
@@ -2,17 +2,14 @@
|
|||||||
|
|
||||||
from airtouch4pyapi import AirTouch
|
from airtouch4pyapi import AirTouch
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_HOST, Platform
|
from homeassistant.const import CONF_HOST, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
|
||||||
from .coordinator import AirtouchDataUpdateCoordinator
|
from .coordinator import AirTouch4ConfigEntry, AirtouchDataUpdateCoordinator
|
||||||
|
|
||||||
PLATFORMS = [Platform.CLIMATE]
|
PLATFORMS = [Platform.CLIMATE]
|
||||||
|
|
||||||
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) -> bool:
|
||||||
"""Set up AirTouch4 from a config entry."""
|
"""Set up AirTouch4 from a config entry."""
|
||||||
@@ -22,7 +19,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirTouch4ConfigEntry) ->
|
|||||||
info = airtouch.GetAcs()
|
info = airtouch.GetAcs()
|
||||||
if not info:
|
if not info:
|
||||||
raise ConfigEntryNotReady
|
raise ConfigEntryNotReady
|
||||||
coordinator = AirtouchDataUpdateCoordinator(hass, airtouch)
|
coordinator = AirtouchDataUpdateCoordinator(hass, entry, airtouch)
|
||||||
await coordinator.async_config_entry_first_refresh()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
entry.runtime_data = coordinator
|
entry.runtime_data = coordinator
|
||||||
|
|
||||||
|
@@ -2,26 +2,34 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from airtouch4pyapi import AirTouch
|
||||||
from airtouch4pyapi.airtouch import AirTouchStatus
|
from airtouch4pyapi.airtouch import AirTouchStatus
|
||||||
|
|
||||||
from homeassistant.components.climate import SCAN_INTERVAL
|
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
type AirTouch4ConfigEntry = ConfigEntry[AirtouchDataUpdateCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
|
class AirtouchDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Class to manage fetching Airtouch data."""
|
"""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."""
|
"""Initialize global Airtouch data updater."""
|
||||||
self.airtouch = airtouch
|
self.airtouch = airtouch
|
||||||
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=entry,
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=SCAN_INTERVAL,
|
update_interval=SCAN_INTERVAL,
|
||||||
)
|
)
|
||||||
|
@@ -18,7 +18,9 @@ from homeassistant.components.binary_sensor import (
|
|||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
import homeassistant.helpers.entity_registry as er
|
||||||
|
|
||||||
|
from .const import _LOGGER, DOMAIN
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
from .entity import AmazonEntity
|
||||||
from .utils import async_update_unique_id
|
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_supported=lambda device, key: device.sensors.get(key) is not None,
|
||||||
is_available_fn=lambda device, key: (
|
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(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -66,6 +104,8 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = entry.runtime_data
|
coordinator = entry.runtime_data
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
|
||||||
# Replace unique id for "detectionState" binary sensor
|
# Replace unique id for "detectionState" binary sensor
|
||||||
await async_update_unique_id(
|
await async_update_unique_id(
|
||||||
hass,
|
hass,
|
||||||
@@ -75,6 +115,16 @@ async def async_setup_entry(
|
|||||||
"detectionState",
|
"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()
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
def _check_device() -> None:
|
def _check_device() -> None:
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "platinum",
|
"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
|
native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None
|
||||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
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(
|
AmazonSensorEntityDescription(
|
||||||
key="temperature",
|
key="temperature",
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
native_unit_of_measurement_fn=lambda device, _key: (
|
native_unit_of_measurement_fn=lambda device, key: (
|
||||||
UnitOfTemperature.CELSIUS
|
UnitOfTemperature.CELSIUS
|
||||||
if device.sensors[_key].scale == "CELSIUS"
|
if key in device.sensors and device.sensors[key].scale == "CELSIUS"
|
||||||
else UnitOfTemperature.FAHRENHEIT
|
else UnitOfTemperature.FAHRENHEIT
|
||||||
),
|
),
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
@@ -18,7 +18,11 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
|
|
||||||
from .coordinator import AmazonConfigEntry
|
from .coordinator import AmazonConfigEntry
|
||||||
from .entity import AmazonEntity
|
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
|
PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@@ -29,7 +33,9 @@ class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
|||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice], bool]
|
||||||
is_available_fn: Callable[[AmazonDevice, str], bool] = lambda device, key: (
|
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
|
method: str
|
||||||
|
|
||||||
@@ -58,6 +64,9 @@ async def async_setup_entry(
|
|||||||
hass, coordinator, SWITCH_DOMAIN, "do_not_disturb", "dnd"
|
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()
|
known_devices: set[str] = set()
|
||||||
|
|
||||||
def _check_device() -> None:
|
def _check_device() -> None:
|
||||||
|
@@ -4,8 +4,10 @@ from collections.abc import Awaitable, Callable, Coroutine
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Any, Concatenate
|
from typing import Any, Concatenate
|
||||||
|
|
||||||
|
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
|
||||||
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
from aioamazondevices.exceptions import CannotConnect, CannotRetrieveData
|
||||||
|
|
||||||
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
import homeassistant.helpers.entity_registry as er
|
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
|
# Update the registry with the new unique_id
|
||||||
entity_registry.async_update_entity(entity_id, new_unique_id=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",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
|
||||||
"requirements": ["brother==5.1.0"],
|
"requirements": ["brother==5.1.1"],
|
||||||
"zeroconf": [
|
"zeroconf": [
|
||||||
{
|
{
|
||||||
"type": "_printer._tcp.local.",
|
"type": "_printer._tcp.local.",
|
||||||
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from asyncio.exceptions import TimeoutError
|
from asyncio.exceptions import TimeoutError
|
||||||
from collections.abc import Mapping
|
from collections.abc import Mapping
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from aiocomelit import (
|
from aiocomelit import (
|
||||||
@@ -27,25 +28,20 @@ from .utils import async_client_session
|
|||||||
DEFAULT_HOST = "192.168.1.252"
|
DEFAULT_HOST = "192.168.1.252"
|
||||||
DEFAULT_PIN = "111111"
|
DEFAULT_PIN = "111111"
|
||||||
|
|
||||||
|
|
||||||
pin_regex = r"^[0-9]{4,10}$"
|
|
||||||
|
|
||||||
USER_SCHEMA = vol.Schema(
|
USER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string,
|
||||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
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),
|
vol.Required(CONF_TYPE, default=BRIDGE): vol.In(DEVICE_TYPE_LIST),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
|
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_PIN): cv.string})
|
||||||
{vol.Required(CONF_PIN): cv.matches_regex(pin_regex)}
|
|
||||||
)
|
|
||||||
STEP_RECONFIGURE = vol.Schema(
|
STEP_RECONFIGURE = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_HOST): cv.string,
|
vol.Required(CONF_HOST): cv.string,
|
||||||
vol.Required(CONF_PORT): cv.port,
|
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
|
api: ComelitCommonApi
|
||||||
|
|
||||||
|
if not re.fullmatch(r"[0-9]{4,10}", data[CONF_PIN]):
|
||||||
|
raise InvalidPin
|
||||||
|
|
||||||
session = await async_client_session(hass)
|
session = await async_client_session(hass)
|
||||||
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
|
||||||
api = ComeliteSerialBridgeApi(
|
api = ComeliteSerialBridgeApi(
|
||||||
@@ -105,6 +104,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
except InvalidPin:
|
||||||
|
errors["base"] = "invalid_pin"
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
@@ -146,6 +147,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
except InvalidPin:
|
||||||
|
errors["base"] = "invalid_pin"
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
@@ -189,6 +192,8 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "cannot_connect"
|
errors["base"] = "cannot_connect"
|
||||||
except InvalidAuth:
|
except InvalidAuth:
|
||||||
errors["base"] = "invalid_auth"
|
errors["base"] = "invalid_auth"
|
||||||
|
except InvalidPin:
|
||||||
|
errors["base"] = "invalid_pin"
|
||||||
except Exception: # noqa: BLE001
|
except Exception: # noqa: BLE001
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors["base"] = "unknown"
|
errors["base"] = "unknown"
|
||||||
@@ -210,3 +215,7 @@ class CannotConnect(HomeAssistantError):
|
|||||||
|
|
||||||
class InvalidAuth(HomeAssistantError):
|
class InvalidAuth(HomeAssistantError):
|
||||||
"""Error to indicate there is invalid auth."""
|
"""Error to indicate there is invalid auth."""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidPin(HomeAssistantError):
|
||||||
|
"""Error to indicate an invalid pin."""
|
||||||
|
@@ -161,7 +161,7 @@ class ComelitSerialBridge(
|
|||||||
entry: ComelitConfigEntry,
|
entry: ComelitConfigEntry,
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
pin: int,
|
pin: str,
|
||||||
session: ClientSession,
|
session: ClientSession,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
@@ -195,7 +195,7 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
|
|||||||
entry: ComelitConfigEntry,
|
entry: ComelitConfigEntry,
|
||||||
host: str,
|
host: str,
|
||||||
port: int,
|
port: int,
|
||||||
pin: int,
|
pin: str,
|
||||||
session: ClientSession,
|
session: ClientSession,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the scanner."""
|
"""Initialize the scanner."""
|
||||||
|
@@ -7,7 +7,14 @@ from typing import Any, cast
|
|||||||
from aiocomelit import ComelitSerialBridgeObject
|
from aiocomelit import ComelitSerialBridgeObject
|
||||||
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.restore_state import RestoreEntity
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
@@ -62,7 +69,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
|||||||
super().__init__(coordinator, device, config_entry_entry_id)
|
super().__init__(coordinator, device, config_entry_entry_id)
|
||||||
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
# Device doesn't provide a status so we assume UNKNOWN at first startup
|
||||||
self._last_action: int | None = None
|
self._last_action: int | None = None
|
||||||
self._last_state: str | None = None
|
|
||||||
|
|
||||||
def _current_action(self, action: str) -> bool:
|
def _current_action(self, action: str) -> bool:
|
||||||
"""Return the current cover action."""
|
"""Return the current cover action."""
|
||||||
@@ -98,7 +104,6 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
|||||||
@bridge_api_call
|
@bridge_api_call
|
||||||
async def _cover_set_state(self, action: int, state: int) -> None:
|
async def _cover_set_state(self, action: int, state: int) -> None:
|
||||||
"""Set desired cover state."""
|
"""Set desired cover state."""
|
||||||
self._last_state = self.state
|
|
||||||
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
await self.coordinator.api.set_device_status(COVER, self._device.index, action)
|
||||||
self.coordinator.data[COVER][self._device.index].status = state
|
self.coordinator.data[COVER][self._device.index].status = state
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
@@ -124,5 +129,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
|
|||||||
|
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
if last_state := await self.async_get_last_state():
|
if (state := await self.async_get_last_state()) is not None:
|
||||||
self._last_state = last_state.state
|
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",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aiocomelit"],
|
"loggers": ["aiocomelit"],
|
||||||
"quality_scale": "platinum",
|
"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%]",
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"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%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
"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%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -514,7 +514,7 @@ class ChatLog:
|
|||||||
"""Set the LLM system prompt."""
|
"""Set the LLM system prompt."""
|
||||||
llm_api: llm.APIInstance | None = None
|
llm_api: llm.APIInstance | None = None
|
||||||
|
|
||||||
if user_llm_hass_api is None:
|
if not user_llm_hass_api:
|
||||||
pass
|
pass
|
||||||
elif isinstance(user_llm_hass_api, llm.API):
|
elif isinstance(user_llm_hass_api, llm.API):
|
||||||
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)
|
llm_api = await user_llm_hass_api.async_get_api_instance(llm_context)
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"quality_scale": "bronze",
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pydaikin"],
|
"loggers": ["pydaikin"],
|
||||||
"requirements": ["pydaikin==2.16.0"],
|
"requirements": ["pydaikin==2.17.1"],
|
||||||
"zeroconf": ["_dkapi._tcp.local."]
|
"zeroconf": ["_dkapi._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["env_canada"],
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"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
|
hass: HomeAssistant, entry: GoogleAssistantSDKConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""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)
|
conversation.async_unset_agent(hass, entry)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
@@ -26,7 +26,7 @@ from homeassistant.components.media_player import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
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.config_entry_oauth2_flow import OAuth2Session
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
@@ -68,7 +68,13 @@ async def async_send_text_commands(
|
|||||||
) -> list[CommandResponse]:
|
) -> list[CommandResponse]:
|
||||||
"""Send text commands to Google Assistant Service."""
|
"""Send text commands to Google Assistant Service."""
|
||||||
# There can only be 1 entry (config_flow has single_instance_allowed)
|
# 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
|
session = entry.runtime_data.session
|
||||||
try:
|
try:
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
"""Support for Google Assistant SDK."""
|
"""Services for the Google Assistant SDK integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
@@ -65,6 +65,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
|
"entry_not_loaded": {
|
||||||
|
"message": "Entry not loaded"
|
||||||
|
},
|
||||||
"grpc_error": {
|
"grpc_error": {
|
||||||
"message": "Failed to communicate with Google Assistant"
|
"message": "Failed to communicate with Google Assistant"
|
||||||
}
|
}
|
||||||
|
@@ -456,6 +456,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
|||||||
"""Initialize the agent."""
|
"""Initialize the agent."""
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.subentry = subentry
|
self.subentry = subentry
|
||||||
|
self.default_model = default_model
|
||||||
self._attr_name = subentry.title
|
self._attr_name = subentry.title
|
||||||
self._genai_client = entry.runtime_data
|
self._genai_client = entry.runtime_data
|
||||||
self._attr_unique_id = subentry.subentry_id
|
self._attr_unique_id = subentry.subentry_id
|
||||||
@@ -489,7 +490,7 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
|||||||
tools = tools or []
|
tools = tools or []
|
||||||
tools.append(Tool(google_search=GoogleSearch()))
|
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>
|
# Avoid INVALID_ARGUMENT Developer instruction is not enabled for <model>
|
||||||
supports_system_instruction = (
|
supports_system_instruction = (
|
||||||
"gemma" not in model_name
|
"gemma" not in model_name
|
||||||
@@ -620,6 +621,13 @@ class GoogleGenerativeAILLMBaseEntity(Entity):
|
|||||||
def create_generate_content_config(self) -> GenerateContentConfig:
|
def create_generate_content_config(self) -> GenerateContentConfig:
|
||||||
"""Create the GenerateContentConfig for the LLM."""
|
"""Create the GenerateContentConfig for the LLM."""
|
||||||
options = self.subentry.data
|
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(
|
return GenerateContentConfig(
|
||||||
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
|
||||||
top_k=options.get(CONF_TOP_K, RECOMMENDED_TOP_K),
|
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",
|
"documentation": "https://www.home-assistant.io/integrations/hassio",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["aiohasupervisor==0.3.3b0"],
|
"requirements": ["aiohasupervisor==0.3.3"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
@@ -5,5 +5,5 @@
|
|||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.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."""
|
"""Mixin for Home Assistant Connect ZBT-2 firmware methods."""
|
||||||
|
|
||||||
context: ConfigFlowContext
|
context: ConfigFlowContext
|
||||||
|
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR]
|
||||||
# `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]
|
|
||||||
|
|
||||||
async def async_step_install_zigbee_firmware(
|
async def async_step_install_zigbee_firmware(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@@ -157,7 +157,7 @@ async def async_setup_entry(
|
|||||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||||
"""Connect ZBT-2 firmware update entity."""
|
"""Connect ZBT-2 firmware update entity."""
|
||||||
|
|
||||||
bootloader_reset_methods = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
bootloader_reset_methods = [ResetTarget.RTS_DTR]
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@@ -1,15 +1,20 @@
|
|||||||
"""Home Assistant Hardware integration helpers."""
|
"""Home Assistant Hardware integration helpers."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
import logging
|
import logging
|
||||||
from typing import Protocol
|
from typing import TYPE_CHECKING, Protocol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||||
|
|
||||||
from . import DATA_COMPONENT
|
from . import DATA_COMPONENT
|
||||||
from .util import FirmwareInfo
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .util import FirmwareInfo
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -51,6 +56,7 @@ class HardwareInfoDispatcher:
|
|||||||
self._notification_callbacks: defaultdict[
|
self._notification_callbacks: defaultdict[
|
||||||
str, set[Callable[[FirmwareInfo], None]]
|
str, set[Callable[[FirmwareInfo], None]]
|
||||||
] = defaultdict(set)
|
] = defaultdict(set)
|
||||||
|
self._active_firmware_updates: dict[str, str] = {}
|
||||||
|
|
||||||
def register_firmware_info_provider(
|
def register_firmware_info_provider(
|
||||||
self, domain: str, platform: HardwareFirmwareInfoModule
|
self, domain: str, platform: HardwareFirmwareInfoModule
|
||||||
@@ -118,6 +124,36 @@ class HardwareInfoDispatcher:
|
|||||||
if fw_info is not None:
|
if fw_info is not None:
|
||||||
yield fw_info
|
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
|
@hass_callback
|
||||||
def async_register_firmware_info_provider(
|
def async_register_firmware_info_provider(
|
||||||
@@ -141,3 +177,42 @@ def async_notify_firmware_info(
|
|||||||
) -> Awaitable[None]:
|
) -> Awaitable[None]:
|
||||||
"""Notify the dispatcher of new firmware information."""
|
"""Notify the dispatcher of new firmware information."""
|
||||||
return hass.data[DATA_COMPONENT].notify_firmware_info(domain, firmware_info)
|
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",
|
"documentation": "https://www.home-assistant.io/integrations/homeassistant_hardware",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"universal-silabs-flasher==0.0.34",
|
"universal-silabs-flasher==0.0.35",
|
||||||
"ha-silabs-firmware-client==0.2.0"
|
"ha-silabs-firmware-client==0.2.0"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -67,7 +67,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"abort": {
|
"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.",
|
"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.",
|
"zha_still_using_stick": "This {model} is in use by the Zigbee Home Automation integration. Please migrate your Zigbee network to another adapter or delete the integration and try again.",
|
||||||
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
"otbr_still_using_stick": "This {model} is in use by the OpenThread Border Router add-on. If you use the Thread network, make sure you have alternative border routers. Uninstall the add-on and try again.",
|
||||||
|
@@ -275,6 +275,7 @@ class BaseFirmwareUpdateEntity(
|
|||||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||||
bootloader_reset_methods=self.bootloader_reset_methods,
|
bootloader_reset_methods=self.bootloader_reset_methods,
|
||||||
progress_callback=self._update_progress,
|
progress_callback=self._update_progress,
|
||||||
|
domain=self._config_entry.domain,
|
||||||
)
|
)
|
||||||
finally:
|
finally:
|
||||||
self._attr_in_progress = False
|
self._attr_in_progress = False
|
||||||
|
@@ -26,6 +26,7 @@ from homeassistant.helpers.singleton import singleton
|
|||||||
|
|
||||||
from . import DATA_COMPONENT
|
from . import DATA_COMPONENT
|
||||||
from .const import (
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
OTBR_ADDON_MANAGER_DATA,
|
OTBR_ADDON_MANAGER_DATA,
|
||||||
OTBR_ADDON_NAME,
|
OTBR_ADDON_NAME,
|
||||||
OTBR_ADDON_SLUG,
|
OTBR_ADDON_SLUG,
|
||||||
@@ -33,6 +34,7 @@ from .const import (
|
|||||||
ZIGBEE_FLASHER_ADDON_NAME,
|
ZIGBEE_FLASHER_ADDON_NAME,
|
||||||
ZIGBEE_FLASHER_ADDON_SLUG,
|
ZIGBEE_FLASHER_ADDON_SLUG,
|
||||||
)
|
)
|
||||||
|
from .helpers import async_firmware_update_context
|
||||||
from .silabs_multiprotocol_addon import (
|
from .silabs_multiprotocol_addon import (
|
||||||
WaitingAddonManager,
|
WaitingAddonManager,
|
||||||
get_multiprotocol_addon_manager,
|
get_multiprotocol_addon_manager,
|
||||||
@@ -359,45 +361,50 @@ async def async_flash_silabs_firmware(
|
|||||||
expected_installed_firmware_type: ApplicationType,
|
expected_installed_firmware_type: ApplicationType,
|
||||||
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
bootloader_reset_methods: Sequence[ResetTarget] = (),
|
||||||
progress_callback: Callable[[int, int], None] | None = None,
|
progress_callback: Callable[[int, int], None] | None = None,
|
||||||
|
*,
|
||||||
|
domain: str = DOMAIN,
|
||||||
) -> FirmwareInfo:
|
) -> FirmwareInfo:
|
||||||
"""Flash firmware to the SiLabs device."""
|
"""Flash firmware to the SiLabs device."""
|
||||||
firmware_info = await guess_firmware_info(hass, device)
|
async with async_firmware_update_context(hass, device, domain):
|
||||||
_LOGGER.debug("Identified firmware info: %s", firmware_info)
|
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(
|
flasher = Flasher(
|
||||||
device=device,
|
device=device,
|
||||||
probe_methods=(
|
probe_methods=(
|
||||||
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
ApplicationType.GECKO_BOOTLOADER.as_flasher_application_type(),
|
||||||
ApplicationType.EZSP.as_flasher_application_type(),
|
ApplicationType.EZSP.as_flasher_application_type(),
|
||||||
ApplicationType.SPINEL.as_flasher_application_type(),
|
ApplicationType.SPINEL.as_flasher_application_type(),
|
||||||
ApplicationType.CPC.as_flasher_application_type(),
|
ApplicationType.CPC.as_flasher_application_type(),
|
||||||
),
|
),
|
||||||
bootloader_reset=tuple(
|
bootloader_reset=tuple(
|
||||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
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,),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if probed_firmware_info is None:
|
async with AsyncExitStack() as stack:
|
||||||
raise HomeAssistantError("Failed to probe the firmware after flashing")
|
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",
|
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aiohomekit", "commentjson"],
|
"loggers": ["aiohomekit", "commentjson"],
|
||||||
"requirements": ["aiohomekit==3.2.18"],
|
"requirements": ["aiohomekit==3.2.19"],
|
||||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -8,13 +8,16 @@ from idasen_ha import Desk
|
|||||||
|
|
||||||
from homeassistant.components import bluetooth
|
from homeassistant.components import bluetooth
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
|
type IdasenDeskConfigEntry = ConfigEntry[IdasenDeskCoordinator]
|
||||||
|
|
||||||
|
UPDATE_DEBOUNCE_TIME = 0.2
|
||||||
|
|
||||||
|
|
||||||
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
||||||
"""Class to manage updates for the Idasen Desk."""
|
"""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
|
hass, _LOGGER, config_entry=config_entry, name=config_entry.title
|
||||||
)
|
)
|
||||||
self.address = address
|
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:
|
async def async_connect(self) -> bool:
|
||||||
"""Connect to desk."""
|
"""Connect to desk."""
|
||||||
@@ -60,3 +76,9 @@ class IdasenDeskCoordinator(DataUpdateCoordinator[int | None]):
|
|||||||
"""Ensure that the desk is connected if that is the expected state."""
|
"""Ensure that the desk is connected if that is the expected state."""
|
||||||
if self._expected_connected:
|
if self._expected_connected:
|
||||||
await self.async_connect()
|
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",
|
"domain": "iometer",
|
||||||
"name": "IOmeter",
|
"name": "IOmeter",
|
||||||
"codeowners": ["@MaestroOnICe"],
|
"codeowners": ["@jukrebs"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/iometer",
|
"documentation": "https://www.home-assistant.io/integrations/iometer",
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["iometer==0.1.0"],
|
"requirements": ["iometer==0.2.0"],
|
||||||
"zeroconf": ["_iometer._tcp.local."]
|
"zeroconf": ["_iometer._tcp.local."]
|
||||||
}
|
}
|
||||||
|
@@ -37,5 +37,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"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.components.media_player import BrowseMedia, MediaClass, MediaType
|
||||||
from homeassistant.core import HomeAssistant, callback
|
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
|
from .const import MEDIA_SOURCE_DATA, URI_SCHEME, URI_SCHEME_REGEX
|
||||||
|
|
||||||
@@ -62,12 +63,15 @@ class MediaSourceItem:
|
|||||||
async def async_browse(self) -> BrowseMediaSource:
|
async def async_browse(self) -> BrowseMediaSource:
|
||||||
"""Browse this item."""
|
"""Browse this item."""
|
||||||
if self.domain is None:
|
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(
|
base = BrowseMediaSource(
|
||||||
domain=None,
|
domain=None,
|
||||||
identifier=None,
|
identifier=None,
|
||||||
media_class=MediaClass.APP,
|
media_class=MediaClass.APP,
|
||||||
media_content_type=MediaType.APPS,
|
media_content_type=MediaType.APPS,
|
||||||
title="Media Sources",
|
title=title,
|
||||||
can_play=False,
|
can_play=False,
|
||||||
can_expand=True,
|
can_expand=True,
|
||||||
children_media_class=MediaClass.APP,
|
children_media_class=MediaClass.APP,
|
||||||
|
@@ -9,5 +9,8 @@
|
|||||||
"unknown_media_source": {
|
"unknown_media_source": {
|
||||||
"message": "Unknown media source: {domain}"
|
"message": "Unknown media source: {domain}"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"sources_default": "Media sources"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
"documentation": "https://www.home-assistant.io/integrations/melcloud",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pymelcloud"],
|
"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
|
DEFAULT_PLATE_COUNT = 4
|
||||||
|
|
||||||
PLATE_COUNT = {
|
PLATE_COUNT = {
|
||||||
|
"KM7575": 6,
|
||||||
"KM7678": 6,
|
"KM7678": 6,
|
||||||
"KM7697": 6,
|
"KM7697": 6,
|
||||||
"KM7878": 6,
|
"KM7878": 6,
|
||||||
|
@@ -208,7 +208,7 @@ class ModbusStructEntity(ModbusBaseEntity, RestoreEntity):
|
|||||||
|
|
||||||
def __process_raw_value(self, entry: float | str | bytes) -> str | None:
|
def __process_raw_value(self, entry: float | str | bytes) -> str | None:
|
||||||
"""Process value from sensor with NaN handling, scaling, offset, min/max etc."""
|
"""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
|
return None
|
||||||
if isinstance(entry, bytes):
|
if isinstance(entry, bytes):
|
||||||
return entry.decode()
|
return entry.decode()
|
||||||
|
@@ -253,6 +253,7 @@ class ModbusHub:
|
|||||||
self._client: (
|
self._client: (
|
||||||
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
AsyncModbusSerialClient | AsyncModbusTcpClient | AsyncModbusUdpClient | None
|
||||||
) = None
|
) = None
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
self.event_connected = asyncio.Event()
|
self.event_connected = asyncio.Event()
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.name = client_config[CONF_NAME]
|
self.name = client_config[CONF_NAME]
|
||||||
@@ -415,7 +416,9 @@ class ModbusHub:
|
|||||||
"""Convert async to sync pymodbus call."""
|
"""Convert async to sync pymodbus call."""
|
||||||
if not self._client:
|
if not self._client:
|
||||||
return None
|
return None
|
||||||
result = await self.low_level_pb_call(unit, address, value, use_call)
|
async with self._lock:
|
||||||
if self._msg_wait:
|
result = await self.low_level_pb_call(unit, address, value, use_call)
|
||||||
await asyncio.sleep(self._msg_wait)
|
if self._msg_wait:
|
||||||
return result
|
# small delay until next request/response
|
||||||
|
await asyncio.sleep(self._msg_wait)
|
||||||
|
return result
|
||||||
|
@@ -188,7 +188,10 @@ class MqttLock(MqttEntity, LockEntity):
|
|||||||
return
|
return
|
||||||
if payload == self._config[CONF_PAYLOAD_RESET]:
|
if payload == self._config[CONF_PAYLOAD_RESET]:
|
||||||
# Reset the state to `unknown`
|
# 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:
|
elif payload in self._valid_states:
|
||||||
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
self._attr_is_locked = payload == self._config[CONF_STATE_LOCKED]
|
||||||
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
self._attr_is_locking = payload == self._config[CONF_STATE_LOCKING]
|
||||||
|
@@ -34,6 +34,7 @@ async def async_setup_entry(
|
|||||||
|
|
||||||
coordinator = NordPoolDataUpdateCoordinator(hass, config_entry)
|
coordinator = NordPoolDataUpdateCoordinator(hass, config_entry)
|
||||||
await coordinator.fetch_data(dt_util.utcnow(), True)
|
await coordinator.fetch_data(dt_util.utcnow(), True)
|
||||||
|
await coordinator.update_listeners(dt_util.utcnow())
|
||||||
if not coordinator.last_update_success:
|
if not coordinator.last_update_success:
|
||||||
raise ConfigEntryNotReady(
|
raise ConfigEntryNotReady(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
|
@@ -44,9 +44,10 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
|
|||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
)
|
)
|
||||||
self.client = NordPoolClient(session=async_get_clientsession(hass))
|
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."""
|
"""Compute next time an update should occur."""
|
||||||
next_hour = dt_util.utcnow() + timedelta(hours=1)
|
next_hour = dt_util.utcnow() + timedelta(hours=1)
|
||||||
next_run = datetime(
|
next_run = datetime(
|
||||||
@@ -56,23 +57,45 @@ class NordPoolDataUpdateCoordinator(DataUpdateCoordinator[DeliveryPeriodsData]):
|
|||||||
next_hour.hour,
|
next_hour.hour,
|
||||||
tzinfo=dt_util.UTC,
|
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
|
return next_run
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
async def async_shutdown(self) -> None:
|
||||||
"""Cancel any scheduled call, and ignore new runs."""
|
"""Cancel any scheduled call, and ignore new runs."""
|
||||||
await super().async_shutdown()
|
await super().async_shutdown()
|
||||||
if self.unsub:
|
if self.data_unsub:
|
||||||
self.unsub()
|
self.data_unsub()
|
||||||
self.unsub = None
|
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:
|
async def fetch_data(self, now: datetime, initial: bool = False) -> None:
|
||||||
"""Fetch data from Nord Pool."""
|
"""Fetch data from Nord Pool."""
|
||||||
self.unsub = async_track_point_in_utc_time(
|
self.data_unsub = async_track_point_in_utc_time(
|
||||||
self.hass, self.fetch_data, self.get_next_interval(dt_util.utcnow())
|
self.hass, self.fetch_data, self.get_next_data_interval(dt_util.utcnow())
|
||||||
)
|
)
|
||||||
if self.config_entry.pref_disable_polling and not initial:
|
if self.config_entry.pref_disable_polling and not initial:
|
||||||
self.async_update_listeners()
|
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
data = await self.handle_data(initial)
|
data = await self.handle_data(initial)
|
||||||
|
@@ -157,7 +157,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
|||||||
) from error
|
) from error
|
||||||
except NordPoolEmptyResponseError:
|
except NordPoolEmptyResponseError:
|
||||||
return {area: [] for area in areas}
|
return {area: [] for area in areas}
|
||||||
except NordPoolError as error:
|
except (NordPoolError, TimeoutError) as error:
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="connection_error",
|
translation_key="connection_error",
|
||||||
|
@@ -307,7 +307,7 @@
|
|||||||
},
|
},
|
||||||
"markdown": {
|
"markdown": {
|
||||||
"name": "Format as 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": {
|
"tags": {
|
||||||
"name": "Tags/Emojis",
|
"name": "Tags/Emojis",
|
||||||
|
@@ -35,7 +35,7 @@ from .const import CONF_DELETE_PERMANENTLY, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
|
|||||||
from .coordinator import OneDriveConfigEntry
|
from .coordinator import OneDriveConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_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
|
TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours
|
||||||
METADATA_VERSION = 2
|
METADATA_VERSION = 2
|
||||||
CACHE_TTL = 300
|
CACHE_TTL = 300
|
||||||
@@ -163,7 +163,10 @@ class OneDriveBackupAgent(BackupAgent):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
backup_file = await LargeFileUploadClient.upload(
|
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:
|
except HashMismatchError as err:
|
||||||
raise BackupAgentError(
|
raise BackupAgentError(
|
||||||
|
@@ -8,5 +8,5 @@
|
|||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["opower"],
|
"loggers": ["opower"],
|
||||||
"quality_scale": "bronze",
|
"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):
|
if device and ("Connect_ZBT-1" in device or "SkyConnect" in device):
|
||||||
return f"Home Assistant Connect ZBT-1 ({discovery_info.name})"
|
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
|
return discovery_info.name
|
||||||
|
|
||||||
|
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["ovoenergy"],
|
"loggers": ["ovoenergy"],
|
||||||
"requirements": ["ovoenergy==2.0.1"]
|
"requirements": ["ovoenergy==3.0.1"]
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/portainer",
|
"documentation": "https://www.home-assistant.io/integrations/portainer",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyportainer==0.1.7"]
|
"requirements": ["pyportainer==1.0.3"]
|
||||||
}
|
}
|
||||||
|
@@ -215,6 +215,7 @@ def create_coordinator_container_vm(
|
|||||||
return DataUpdateCoordinator(
|
return DataUpdateCoordinator(
|
||||||
hass,
|
hass,
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
|
config_entry=None,
|
||||||
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
|
name=f"proxmox_coordinator_{host_name}_{node_name}_{vm_id}",
|
||||||
update_method=async_update_data,
|
update_method=async_update_data,
|
||||||
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
update_interval=timedelta(seconds=UPDATE_INTERVAL),
|
||||||
|
@@ -16,7 +16,6 @@ ATTR_HTML: Final = "html"
|
|||||||
ATTR_CALLBACK_URL: Final = "callback_url"
|
ATTR_CALLBACK_URL: Final = "callback_url"
|
||||||
ATTR_EXPIRE: Final = "expire"
|
ATTR_EXPIRE: Final = "expire"
|
||||||
ATTR_TTL: Final = "ttl"
|
ATTR_TTL: Final = "ttl"
|
||||||
ATTR_DATA: Final = "data"
|
|
||||||
ATTR_TIMESTAMP: Final = "timestamp"
|
ATTR_TIMESTAMP: Final = "timestamp"
|
||||||
|
|
||||||
CONF_USER_KEY: Final = "user_key"
|
CONF_USER_KEY: Final = "user_key"
|
||||||
|
@@ -67,7 +67,7 @@ class PushoverNotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
# Extract params from data dict
|
# Extract params from data dict
|
||||||
title = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
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 = data.get(ATTR_URL)
|
||||||
url_title = data.get(ATTR_URL_TITLE)
|
url_title = data.get(ATTR_URL_TITLE)
|
||||||
priority = data.get(ATTR_PRIORITY)
|
priority = data.get(ATTR_PRIORITY)
|
||||||
|
@@ -39,6 +39,23 @@ from .renault_vehicle import COORDINATORS, RenaultVehicleProxy
|
|||||||
LOGGER = logging.getLogger(__name__)
|
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:
|
class RenaultHub:
|
||||||
"""Handle account communication with Renault servers."""
|
"""Handle account communication with Renault servers."""
|
||||||
|
|
||||||
@@ -84,49 +101,48 @@ class RenaultHub:
|
|||||||
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
account_id: str = config_entry.data[CONF_KAMEREON_ACCOUNT_ID]
|
||||||
|
|
||||||
self._account = await self._client.get_api_account(account_id)
|
self._account = await self._client.get_api_account(account_id)
|
||||||
vehicles = await self._account.get_vehicles()
|
vehicle_links = await _get_filtered_vehicles(self._account)
|
||||||
if vehicles.vehicleLinks:
|
if not vehicle_links:
|
||||||
if any(
|
LOGGER.debug(
|
||||||
vehicle_link.vehicleDetails is None
|
"No valid vehicle details found for account_id: %s", account_id
|
||||||
for vehicle_link in vehicles.vehicleLinks
|
)
|
||||||
):
|
raise ConfigEntryNotReady(
|
||||||
raise ConfigEntryNotReady(
|
"Failed to retrieve vehicle details from Renault servers"
|
||||||
"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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
device_registry = dr.async_get(self._hass)
|
num_call_per_scan = len(COORDINATORS) * len(vehicle_links)
|
||||||
await asyncio.gather(
|
scan_interval = timedelta(
|
||||||
*(
|
seconds=(3600 * num_call_per_scan) / MAX_CALLS_PER_HOURS
|
||||||
self.async_initialise_vehicle(
|
)
|
||||||
vehicle_link,
|
|
||||||
self._account,
|
|
||||||
scan_interval,
|
|
||||||
config_entry,
|
|
||||||
device_registry,
|
|
||||||
)
|
|
||||||
for vehicle_link in vehicles.vehicleLinks
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# all vehicles have been initiated with the right number of active coordinators
|
device_registry = dr.async_get(self._hass)
|
||||||
num_call_per_scan = 0
|
await asyncio.gather(
|
||||||
for vehicle_link in vehicles.vehicleLinks:
|
*(
|
||||||
|
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)]
|
vehicle = self._vehicles[str(vehicle_link.vin)]
|
||||||
num_call_per_scan += len(vehicle.coordinators)
|
vehicle.update_scan_interval(new_scan_interval)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
async def async_initialise_vehicle(
|
async def async_initialise_vehicle(
|
||||||
self,
|
self,
|
||||||
@@ -164,10 +180,10 @@ class RenaultHub:
|
|||||||
"""Get Kamereon account ids."""
|
"""Get Kamereon account ids."""
|
||||||
accounts = []
|
accounts = []
|
||||||
for account in await self._client.get_api_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.
|
# Only add the account if it has linked vehicles.
|
||||||
if vehicles.vehicleLinks:
|
if vehicle_links:
|
||||||
accounts.append(account.account_id)
|
accounts.append(account.account_id)
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
|
@@ -19,5 +19,5 @@
|
|||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["reolink_aio"],
|
"loggers": ["reolink_aio"],
|
||||||
"quality_scale": "platinum",
|
"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
|
assert self._client
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
try:
|
try:
|
||||||
await self._client.request_code()
|
await self._client.request_code_v4()
|
||||||
except RoborockAccountDoesNotExist:
|
except RoborockAccountDoesNotExist:
|
||||||
errors["base"] = "invalid_email"
|
errors["base"] = "invalid_email"
|
||||||
except RoborockUrlException:
|
except RoborockUrlException:
|
||||||
@@ -111,7 +111,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
code = user_input[CONF_ENTRY_CODE]
|
code = user_input[CONF_ENTRY_CODE]
|
||||||
_LOGGER.debug("Logging into Roborock account using email provided code")
|
_LOGGER.debug("Logging into Roborock account using email provided code")
|
||||||
try:
|
try:
|
||||||
user_data = await self._client.code_login(code)
|
user_data = await self._client.code_login_v4(code)
|
||||||
except RoborockInvalidCode:
|
except RoborockInvalidCode:
|
||||||
errors["base"] = "invalid_code"
|
errors["base"] = "invalid_code"
|
||||||
except RoborockException:
|
except RoborockException:
|
||||||
@@ -129,7 +129,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()}
|
reauth_entry, data_updates={CONF_USER_DATA: user_data.as_dict()}
|
||||||
)
|
)
|
||||||
self._abort_if_unique_id_configured(error="already_configured_account")
|
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(
|
return self.async_show_form(
|
||||||
step_id="code",
|
step_id="code",
|
||||||
@@ -176,7 +176,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
return await self.async_step_code()
|
return await self.async_step_code()
|
||||||
return self.async_show_form(step_id="reauth_confirm", errors=errors)
|
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
|
self, client: RoborockApiClient, username: str, user_data: UserData
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Finished config flow and create entry."""
|
"""Finished config flow and create entry."""
|
||||||
@@ -185,7 +185,7 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
data={
|
data={
|
||||||
CONF_USERNAME: username,
|
CONF_USERNAME: username,
|
||||||
CONF_USER_DATA: user_data.as_dict(),
|
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"],
|
"loggers": ["roborock"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"python-roborock==2.47.1",
|
"python-roborock==2.50.2",
|
||||||
"vacuum-map-parser-roborock==0.1.4"
|
"vacuum-map-parser-roborock==0.1.4"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -377,8 +377,10 @@
|
|||||||
"max": "Max",
|
"max": "Max",
|
||||||
"high": "[%key:common::state::high%]",
|
"high": "[%key:common::state::high%]",
|
||||||
"intense": "Intense",
|
"intense": "Intense",
|
||||||
|
"extreme": "Extreme",
|
||||||
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
"custom": "[%key:component::roborock::entity::select::mop_mode::state::custom%]",
|
||||||
"custom_water_flow": "Custom water flow",
|
"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%]"
|
"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 (
|
from .const import (
|
||||||
CONF_OUTPUT_NUMBER,
|
CONF_OUTPUT_NUMBER,
|
||||||
|
CONF_OUTPUTS,
|
||||||
CONF_ZONE_NUMBER,
|
CONF_ZONE_NUMBER,
|
||||||
CONF_ZONE_TYPE,
|
CONF_ZONE_TYPE,
|
||||||
|
CONF_ZONES,
|
||||||
SIGNAL_OUTPUTS_UPDATED,
|
SIGNAL_OUTPUTS_UPDATED,
|
||||||
SIGNAL_ZONES_UPDATED,
|
SIGNAL_ZONES_UPDATED,
|
||||||
SUBENTRY_TYPE_OUTPUT,
|
SUBENTRY_TYPE_OUTPUT,
|
||||||
@@ -49,7 +51,7 @@ async def async_setup_entry(
|
|||||||
zone_num,
|
zone_num,
|
||||||
zone_name,
|
zone_name,
|
||||||
zone_type,
|
zone_type,
|
||||||
SUBENTRY_TYPE_ZONE,
|
CONF_ZONES,
|
||||||
SIGNAL_ZONES_UPDATED,
|
SIGNAL_ZONES_UPDATED,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -73,7 +75,7 @@ async def async_setup_entry(
|
|||||||
output_num,
|
output_num,
|
||||||
output_name,
|
output_name,
|
||||||
ouput_type,
|
ouput_type,
|
||||||
SUBENTRY_TYPE_OUTPUT,
|
CONF_OUTPUTS,
|
||||||
SIGNAL_OUTPUTS_UPDATED,
|
SIGNAL_OUTPUTS_UPDATED,
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
@@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"config_subentries": {
|
"config_subentries": {
|
||||||
"partition": {
|
"partition": {
|
||||||
|
"entry_type": "Partition",
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add partition"
|
"user": "Add partition"
|
||||||
},
|
},
|
||||||
@@ -57,6 +58,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zone": {
|
"zone": {
|
||||||
|
"entry_type": "Zone",
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add zone"
|
"user": "Add zone"
|
||||||
},
|
},
|
||||||
@@ -91,6 +93,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"output": {
|
"output": {
|
||||||
|
"entry_type": "Output",
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add output"
|
"user": "Add output"
|
||||||
},
|
},
|
||||||
@@ -125,6 +128,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"switchable_output": {
|
"switchable_output": {
|
||||||
|
"entry_type": "Switchable output",
|
||||||
"initiate_flow": {
|
"initiate_flow": {
|
||||||
"user": "Add switchable output"
|
"user": "Add switchable output"
|
||||||
},
|
},
|
||||||
|
@@ -6,5 +6,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["sharkiq"],
|
"loggers": ["sharkiq"],
|
||||||
"requirements": ["sharkiq==1.4.0"]
|
"requirements": ["sharkiq==1.4.2"]
|
||||||
}
|
}
|
||||||
|
@@ -319,7 +319,7 @@ RPC_SENSORS: Final = {
|
|||||||
),
|
),
|
||||||
"presencezone_state": RpcBinarySensorDescription(
|
"presencezone_state": RpcBinarySensorDescription(
|
||||||
key="presencezone",
|
key="presencezone",
|
||||||
sub_key="state",
|
sub_key="value",
|
||||||
name="Occupancy",
|
name="Occupancy",
|
||||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||||
entity_class=RpcPresenceBinarySensor,
|
entity_class=RpcPresenceBinarySensor,
|
||||||
|
@@ -226,6 +226,8 @@ class RpcShellyCover(ShellyRpcEntity, CoverEntity):
|
|||||||
def _update_callback(self) -> None:
|
def _update_callback(self) -> None:
|
||||||
"""Handle device update. Use a task when opening/closing is in progress."""
|
"""Handle device update. Use a task when opening/closing is in progress."""
|
||||||
super()._update_callback()
|
super()._update_callback()
|
||||||
|
if not self.coordinator.device.initialized:
|
||||||
|
return
|
||||||
if self.is_closing or self.is_opening:
|
if self.is_closing or self.is_opening:
|
||||||
self.launch_update_task()
|
self.launch_update_task()
|
||||||
|
|
||||||
|
@@ -692,27 +692,25 @@ def async_remove_orphaned_entities(
|
|||||||
"""Remove orphaned entities."""
|
"""Remove orphaned entities."""
|
||||||
orphaned_entities = []
|
orphaned_entities = []
|
||||||
entity_reg = er.async_get(hass)
|
entity_reg = er.async_get(hass)
|
||||||
device_reg = dr.async_get(hass)
|
|
||||||
|
|
||||||
if not (
|
entities = er.async_entries_for_config_entry(entity_reg, config_entry_id)
|
||||||
devices := device_reg.devices.get_devices_for_config_entry_id(config_entry_id)
|
for entity in entities:
|
||||||
):
|
if not entity.entity_id.startswith(platform):
|
||||||
return
|
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:
|
key = match.group()
|
||||||
entities = er.async_entries_for_device(entity_reg, device.id, True)
|
if key not in keys:
|
||||||
for entity in entities:
|
LOGGER.debug(
|
||||||
if not entity.entity_id.startswith(platform):
|
"Found orphaned Shelly entity: %s, unique id: %s",
|
||||||
continue
|
entity.entity_id,
|
||||||
if key_suffix is not None and key_suffix not in entity.unique_id:
|
entity.unique_id,
|
||||||
continue
|
)
|
||||||
# we are looking for the component ID, e.g. boolean:201, em1data:1
|
orphaned_entities.append(entity.unique_id.split("-", 1)[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])
|
|
||||||
|
|
||||||
if orphaned_entities:
|
if orphaned_entities:
|
||||||
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
|
async_remove_shelly_rpc_entities(hass, platform, mac, orphaned_entities)
|
||||||
|
@@ -100,8 +100,9 @@ ATTR_PIN_VALUE = "pin"
|
|||||||
ATTR_TIMESTAMP = "timestamp"
|
ATTR_TIMESTAMP = "timestamp"
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
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_EVENT = "SIMPLISAFE_EVENT"
|
||||||
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
EVENT_SIMPLISAFE_NOTIFICATION = "SIMPLISAFE_NOTIFICATION"
|
||||||
@@ -419,6 +420,7 @@ class SimpliSafe:
|
|||||||
self._api = api
|
self._api = api
|
||||||
self._hass = hass
|
self._hass = hass
|
||||||
self._system_notifications: dict[int, set[SystemNotification]] = {}
|
self._system_notifications: dict[int, set[SystemNotification]] = {}
|
||||||
|
self._websocket_reconnect_retries: int = 0
|
||||||
self._websocket_reconnect_task: asyncio.Task | None = None
|
self._websocket_reconnect_task: asyncio.Task | None = None
|
||||||
self.entry = entry
|
self.entry = entry
|
||||||
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
self.initial_event_to_use: dict[int, dict[str, Any]] = {}
|
||||||
@@ -469,6 +471,8 @@ class SimpliSafe:
|
|||||||
"""Start a websocket reconnection loop."""
|
"""Start a websocket reconnection loop."""
|
||||||
assert self._api.websocket
|
assert self._api.websocket
|
||||||
|
|
||||||
|
self._websocket_reconnect_retries += 1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self._api.websocket.async_connect()
|
await self._api.websocket.async_connect()
|
||||||
await self._api.websocket.async_listen()
|
await self._api.websocket.async_listen()
|
||||||
@@ -479,9 +483,21 @@ class SimpliSafe:
|
|||||||
LOGGER.error("Failed to connect to websocket: %s", err)
|
LOGGER.error("Failed to connect to websocket: %s", err)
|
||||||
except Exception as err: # noqa: BLE001
|
except Exception as err: # noqa: BLE001
|
||||||
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
|
LOGGER.error("Unknown exception while connecting to websocket: %s", err)
|
||||||
|
else:
|
||||||
|
self._websocket_reconnect_retries = 0
|
||||||
|
|
||||||
LOGGER.debug("Reconnecting to websocket")
|
if self._websocket_reconnect_retries >= WEBSOCKET_RECONNECT_RETRIES:
|
||||||
await self._async_cancel_websocket_loop()
|
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._websocket_reconnect_task = self._hass.async_create_task(
|
||||||
self._async_start_websocket_loop()
|
self._async_start_websocket_loop()
|
||||||
)
|
)
|
||||||
|
@@ -5,14 +5,14 @@
|
|||||||
"description": "Refer to the documentation on getting your Slack API key.",
|
"description": "Refer to the documentation on getting your Slack API key.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||||
"default_channel": "Default Channel",
|
"default_channel": "Default channel",
|
||||||
"icon": "Icon",
|
"icon": "Icon",
|
||||||
"username": "[%key:common::config_flow::data::username%]"
|
"username": "[%key:common::config_flow::data::username%]"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"api_key": "The Slack API token to use for sending Slack messages.",
|
"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.",
|
"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."
|
"username": "Home Assistant will post to Slack using the username specified."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -109,6 +109,8 @@ PRESET_MODE_TO_HA = {
|
|||||||
"quiet": "quiet",
|
"quiet": "quiet",
|
||||||
"longWind": "long_wind",
|
"longWind": "long_wind",
|
||||||
"smart": "smart",
|
"smart": "smart",
|
||||||
|
"motionIndirect": "motion_indirect",
|
||||||
|
"motionDirect": "motion_direct",
|
||||||
}
|
}
|
||||||
|
|
||||||
HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()}
|
HA_MODE_TO_PRESET_MODE = {v: k for k, v in PRESET_MODE_TO_HA.items()}
|
||||||
|
@@ -31,6 +31,17 @@
|
|||||||
"default": "mdi:stop"
|
"default": "mdi:stop"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"climate": {
|
||||||
|
"air_conditioner": {
|
||||||
|
"state_attributes": {
|
||||||
|
"fan_mode": {
|
||||||
|
"state": {
|
||||||
|
"turbo": "mdi:wind-power"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"number": {
|
"number": {
|
||||||
"washer_rinse_cycles": {
|
"washer_rinse_cycles": {
|
||||||
"default": "mdi:waves-arrow-up"
|
"default": "mdi:waves-arrow-up"
|
||||||
|
@@ -30,5 +30,5 @@
|
|||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pysmartthings"],
|
"loggers": ["pysmartthings"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pysmartthings==3.3.0"]
|
"requirements": ["pysmartthings==3.3.1"]
|
||||||
}
|
}
|
||||||
|
@@ -1151,8 +1151,11 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
and (
|
and (
|
||||||
not description.exists_fn
|
not description.exists_fn
|
||||||
or description.exists_fn(
|
or (
|
||||||
device.status[MAIN][capability][attribute]
|
component == MAIN
|
||||||
|
and description.exists_fn(
|
||||||
|
device.status[MAIN][capability][attribute]
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
and (
|
and (
|
||||||
|
@@ -87,7 +87,14 @@
|
|||||||
"wind_free_sleep": "WindFree sleep",
|
"wind_free_sleep": "WindFree sleep",
|
||||||
"quiet": "Quiet",
|
"quiet": "Quiet",
|
||||||
"long_wind": "Long wind",
|
"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": {
|
"zigbee_type": {
|
||||||
"default": "mdi:zigbee"
|
"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(
|
SmSwitchEntityDescription(
|
||||||
key="auto_zigbee_update",
|
key="auto_zigbee_update",
|
||||||
translation_key="auto_zigbee_update",
|
translation_key="auto_zigbee_update",
|
||||||
entity_category=EntityCategory.CONFIG,
|
|
||||||
setting=Settings.ZB_AUTOUPDATE,
|
setting=Settings.ZB_AUTOUPDATE,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
state_fn=lambda x: x.auto_zigbee,
|
state_fn=lambda x: x.auto_zigbee,
|
||||||
@@ -83,6 +82,7 @@ class SmSwitch(SmEntity, SwitchEntity):
|
|||||||
coordinator: SmDataUpdateCoordinator
|
coordinator: SmDataUpdateCoordinator
|
||||||
entity_description: SmSwitchEntityDescription
|
entity_description: SmSwitchEntityDescription
|
||||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
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:
|
if player.player_id in entry.runtime_data.known_player_ids:
|
||||||
await player.async_update()
|
await player.async_update()
|
||||||
async_dispatcher_send(
|
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:
|
else:
|
||||||
_LOGGER.debug("Adding new entity: %s", player)
|
_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()
|
await player_coordinator.async_refresh()
|
||||||
entry.runtime_data.known_player_ids.add(player.player_id)
|
entry.runtime_data.known_player_ids.add(player.player_id)
|
||||||
async_dispatcher_send(
|
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():
|
if players := await lms.async_get_players():
|
||||||
|
@@ -132,7 +132,9 @@ async def async_setup_entry(
|
|||||||
async_add_entities(entities)
|
async_add_entities(entities)
|
||||||
|
|
||||||
entry.async_on_unload(
|
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
|
# start listening for restored players
|
||||||
self._remove_dispatcher = async_dispatcher_connect(
|
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] = (
|
alarm_dict: dict[str, Alarm] = (
|
||||||
|
@@ -175,7 +175,9 @@ async def async_setup_entry(
|
|||||||
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
|
async_add_entities([SqueezeBoxMediaPlayerEntity(coordinator)])
|
||||||
|
|
||||||
entry.async_on_unload(
|
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
|
# Register entity services
|
||||||
|
@@ -89,7 +89,9 @@ async def async_setup_entry(
|
|||||||
async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)])
|
async_add_entities([SqueezeBoxAlarmsEnabledEntity(coordinator)])
|
||||||
|
|
||||||
entry.async_on_unload(
|
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_conf: Mapping[str, Any] = {}
|
||||||
self.reauth_reason: str | None = None
|
self.reauth_reason: str | None = None
|
||||||
self.shares: list[SynoFileSharedFolder] | None = None
|
self.shares: list[SynoFileSharedFolder] | None = None
|
||||||
|
self.api: SynologyDSM | None = None
|
||||||
|
|
||||||
def _show_form(
|
def _show_form(
|
||||||
self,
|
self,
|
||||||
@@ -156,6 +157,7 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
description_placeholders = {}
|
description_placeholders = {}
|
||||||
data_schema = None
|
data_schema = None
|
||||||
|
self.api = None
|
||||||
|
|
||||||
if step_id == "link":
|
if step_id == "link":
|
||||||
user_input.update(self.discovered_conf)
|
user_input.update(self.discovered_conf)
|
||||||
@@ -194,14 +196,21 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
port = DEFAULT_PORT
|
port = DEFAULT_PORT
|
||||||
|
|
||||||
session = async_get_clientsession(self.hass, verify_ssl)
|
if self.api is None:
|
||||||
api = SynologyDSM(
|
session = async_get_clientsession(self.hass, verify_ssl)
|
||||||
session, host, port, username, password, use_ssl, timeout=DEFAULT_TIMEOUT
|
self.api = SynologyDSM(
|
||||||
)
|
session,
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
use_ssl,
|
||||||
|
timeout=DEFAULT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
errors = {}
|
errors = {}
|
||||||
try:
|
try:
|
||||||
serial = await _login_and_fetch_syno_info(api, otp_code)
|
serial = await _login_and_fetch_syno_info(self.api, otp_code)
|
||||||
except SynologyDSMLogin2SARequiredException:
|
except SynologyDSMLogin2SARequiredException:
|
||||||
return await self.async_step_2sa(user_input)
|
return await self.async_step_2sa(user_input)
|
||||||
except SynologyDSMLogin2SAFailedException:
|
except SynologyDSMLogin2SAFailedException:
|
||||||
@@ -221,10 +230,11 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
errors["base"] = "missing_data"
|
errors["base"] = "missing_data"
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
|
self.api = None
|
||||||
return self._show_form(step_id, user_input, errors)
|
return self._show_form(step_id, user_input, errors)
|
||||||
|
|
||||||
with suppress(*SYNOLOGY_CONNECTION_EXCEPTIONS):
|
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:
|
if self.shares and not backup_path:
|
||||||
return await self.async_step_backup_share(user_input)
|
return await self.async_step_backup_share(user_input)
|
||||||
@@ -239,14 +249,14 @@ class SynologyDSMFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
CONF_VERIFY_SSL: verify_ssl,
|
CONF_VERIFY_SSL: verify_ssl,
|
||||||
CONF_USERNAME: username,
|
CONF_USERNAME: username,
|
||||||
CONF_PASSWORD: password,
|
CONF_PASSWORD: password,
|
||||||
CONF_MAC: api.network.macs,
|
CONF_MAC: self.api.network.macs,
|
||||||
}
|
}
|
||||||
config_options = {
|
config_options = {
|
||||||
CONF_BACKUP_PATH: backup_path,
|
CONF_BACKUP_PATH: backup_path,
|
||||||
CONF_BACKUP_SHARE: backup_share,
|
CONF_BACKUP_SHARE: backup_share,
|
||||||
}
|
}
|
||||||
if otp_code:
|
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):
|
if user_input.get(CONF_DISKS):
|
||||||
config_data[CONF_DISKS] = user_input[CONF_DISKS]
|
config_data[CONF_DISKS] = user_input[CONF_DISKS]
|
||||||
if user_input.get(CONF_VOLUMES):
|
if user_input.get(CONF_VOLUMES):
|
||||||
|
@@ -336,6 +336,7 @@ BASE_SENSOR_TYPES: tuple[SystemBridgeSensorEntityDescription, ...] = (
|
|||||||
key="power_usage",
|
key="power_usage",
|
||||||
translation_key="power_usage",
|
translation_key="power_usage",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
device_class=SensorDeviceClass.POWER,
|
||||||
native_unit_of_measurement=UnitOfPower.WATT,
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
icon="mdi:power-plug",
|
icon="mdi:power-plug",
|
||||||
@@ -577,7 +578,6 @@ async def async_setup_entry(
|
|||||||
key=f"gpu_{gpu.id}_power_usage",
|
key=f"gpu_{gpu.id}_power_usage",
|
||||||
name=f"{gpu.name} power usage",
|
name=f"{gpu.name} power usage",
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
device_class=SensorDeviceClass.POWER,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
native_unit_of_measurement=UnitOfPower.WATT,
|
native_unit_of_measurement=UnitOfPower.WATT,
|
||||||
value=lambda data, k=index: gpu_power_usage(data, k),
|
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):
|
def _set_state(self, state, _=None):
|
||||||
"""Set up auto off."""
|
"""Set up auto off."""
|
||||||
self._attr_is_on = state
|
self._attr_is_on = state
|
||||||
|
self._delay_cancel = None
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
if not state:
|
if not state:
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["tibber"],
|
"loggers": ["tibber"],
|
||||||
"requirements": ["pyTibber==0.32.1"]
|
"requirements": ["pyTibber==0.32.2"]
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
@@ -149,8 +150,9 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
|||||||
raise DeviceNotFound("Unable to connect to device") from exc
|
raise DeviceNotFound("Unable to connect to device") from exc
|
||||||
|
|
||||||
try:
|
try:
|
||||||
packet_a0 = await client.read(PacketA0Notify)
|
async with asyncio.timeout(10):
|
||||||
except (BleakError, DecodeError) as exc:
|
packet_a0 = await client.read(PacketA0Notify)
|
||||||
|
except (BleakError, DecodeError, TimeoutError) as exc:
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
raise DeviceFailed(f"Device failed {exc}") from exc
|
raise DeviceFailed(f"Device failed {exc}") from exc
|
||||||
|
|
||||||
@@ -215,9 +217,19 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
|||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_request_refresh_soon(self) -> None:
|
def _async_request_refresh_soon(self) -> None:
|
||||||
self.config_entry.async_create_task(
|
"""Request a refresh in the near future.
|
||||||
self.hass, self.async_request_refresh(), eager_start=False
|
|
||||||
)
|
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
|
@callback
|
||||||
def _disconnected_callback(self) -> None:
|
def _disconnected_callback(self) -> None:
|
||||||
|
@@ -300,9 +300,10 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
|||||||
self._current_state is not None
|
self._current_state is not None
|
||||||
and (current_state := self.device.status.get(self._current_state))
|
and (current_state := self.device.status.get(self._current_state))
|
||||||
is not None
|
is not None
|
||||||
|
and current_state != "stop"
|
||||||
):
|
):
|
||||||
return self.entity_description.current_state_inverse is not (
|
return self.entity_description.current_state_inverse is not (
|
||||||
current_state in (True, "fully_close")
|
current_state in (True, "close", "fully_close")
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
@@ -100,8 +100,9 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
|||||||
"""Return the currently set speed."""
|
"""Return the currently set speed."""
|
||||||
|
|
||||||
current_level = self.device.state.fan_level
|
current_level = self.device.state.fan_level
|
||||||
|
|
||||||
if self.device.state.mode == VS_FAN_MODE_MANUAL and current_level is not None:
|
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(
|
return ordered_list_item_to_percentage(
|
||||||
self.device.fan_levels, current_level
|
self.device.fan_levels, current_level
|
||||||
)
|
)
|
||||||
@@ -211,17 +212,17 @@ class VeSyncFanHA(VeSyncBaseEntity, FanEntity):
|
|||||||
await self.device.turn_on()
|
await self.device.turn_on()
|
||||||
|
|
||||||
if preset_mode == VS_FAN_MODE_AUTO:
|
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:
|
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:
|
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:
|
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:
|
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:
|
elif preset_mode == VS_FAN_MODE_NORMAL:
|
||||||
success = await self.device.normal_mode()
|
success = await self.device.set_normal_mode()
|
||||||
if not success:
|
if not success:
|
||||||
raise HomeAssistantError(self.device.last_response.message)
|
raise HomeAssistantError(self.device.last_response.message)
|
||||||
|
|
||||||
|
@@ -13,5 +13,5 @@
|
|||||||
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
"documentation": "https://www.home-assistant.io/integrations/vesync",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pyvesync"],
|
"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
|
DEFAULT_CACHE_DURATION = 60
|
||||||
|
|
||||||
|
VICARE_BAR = "bar"
|
||||||
|
VICARE_CUBIC_METER = "cubicMeter"
|
||||||
|
VICARE_KW = "kilowatt"
|
||||||
|
VICARE_KWH = "kilowattHour"
|
||||||
VICARE_PERCENT = "percent"
|
VICARE_PERCENT = "percent"
|
||||||
VICARE_W = "watt"
|
VICARE_W = "watt"
|
||||||
VICARE_KW = "kilowatt"
|
|
||||||
VICARE_WH = "wattHour"
|
VICARE_WH = "wattHour"
|
||||||
VICARE_KWH = "kilowattHour"
|
|
||||||
VICARE_CUBIC_METER = "cubicMeter"
|
|
||||||
|
|
||||||
|
|
||||||
class HeatingType(enum.Enum):
|
class HeatingType(enum.Enum):
|
||||||
|
@@ -41,6 +41,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
VICARE_BAR,
|
||||||
VICARE_CUBIC_METER,
|
VICARE_CUBIC_METER,
|
||||||
VICARE_KW,
|
VICARE_KW,
|
||||||
VICARE_KWH,
|
VICARE_KWH,
|
||||||
@@ -62,20 +63,22 @@ from .utils import (
|
|||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
VICARE_UNIT_TO_DEVICE_CLASS = {
|
VICARE_UNIT_TO_DEVICE_CLASS = {
|
||||||
VICARE_WH: SensorDeviceClass.ENERGY,
|
VICARE_BAR: SensorDeviceClass.PRESSURE,
|
||||||
VICARE_KWH: SensorDeviceClass.ENERGY,
|
|
||||||
VICARE_W: SensorDeviceClass.POWER,
|
|
||||||
VICARE_KW: SensorDeviceClass.POWER,
|
|
||||||
VICARE_CUBIC_METER: SensorDeviceClass.GAS,
|
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_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_PERCENT: PERCENTAGE,
|
||||||
VICARE_W: UnitOfPower.WATT,
|
VICARE_W: UnitOfPower.WATT,
|
||||||
VICARE_KW: UnitOfPower.KILO_WATT,
|
|
||||||
VICARE_WH: UnitOfEnergy.WATT_HOUR,
|
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.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
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 (
|
from homeassistant.helpers.selector import (
|
||||||
SelectOptionDict,
|
SelectOptionDict,
|
||||||
SelectSelector,
|
SelectSelector,
|
||||||
@@ -69,7 +69,7 @@ class VictronRemoteMonitoringFlowHandler(ConfigFlow, domain=DOMAIN):
|
|||||||
"""
|
"""
|
||||||
client = VictronVRMClient(
|
client = VictronVRMClient(
|
||||||
token=api_token,
|
token=api_token,
|
||||||
client_session=get_async_client(self.hass),
|
client_session=async_get_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
sites = await client.users.list_sites()
|
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."""
|
"""Validate access to the selected site and return its data."""
|
||||||
client = VictronVRMClient(
|
client = VictronVRMClient(
|
||||||
token=api_token,
|
token=api_token,
|
||||||
client_session=get_async_client(self.hass),
|
client_session=async_get_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
site_data = await client.users.get_site(site_id)
|
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.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER
|
from .const import CONF_API_TOKEN, CONF_SITE_ID, DOMAIN, LOGGER
|
||||||
@@ -26,8 +26,8 @@ class VRMForecastStore:
|
|||||||
"""Class to hold the forecast data."""
|
"""Class to hold the forecast data."""
|
||||||
|
|
||||||
site_id: int
|
site_id: int
|
||||||
solar: ForecastAggregations
|
solar: ForecastAggregations | None
|
||||||
consumption: ForecastAggregations
|
consumption: ForecastAggregations | None
|
||||||
|
|
||||||
|
|
||||||
async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore:
|
async def get_forecast(client: VictronVRMClient, site_id: int) -> VRMForecastStore:
|
||||||
@@ -75,7 +75,7 @@ class VictronRemoteMonitoringDataUpdateCoordinator(
|
|||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
self.client = VictronVRMClient(
|
self.client = VictronVRMClient(
|
||||||
token=config_entry.data[CONF_API_TOKEN],
|
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]
|
self.site_id = config_entry.data[CONF_SITE_ID]
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@@ -7,5 +7,5 @@
|
|||||||
"integration_type": "service",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"quality_scale": "bronze",
|
"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