forked from home-assistant/core
Compare commits
189 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e73bcc73b5 | |||
| c02707a90f | |||
| 232f853d68 | |||
| 91e296a0c8 | |||
| bcedb06862 | |||
| 2ab32220ed | |||
| 273ccb3929 | |||
| caaa4d5f35 | |||
| 0cf1fd1d41 | |||
| 5ee39df330 | |||
| cc972d20f6 | |||
| e0f32cfd54 | |||
| 6384c800c3 | |||
| 82de2ed8e1 | |||
| af72d1854f | |||
| 0cff7cbccd | |||
| 6f4e16eed1 | |||
| 66be2f9240 | |||
| b6c8718ae4 | |||
| c8b70cc0fb | |||
| 6d1f621e55 | |||
| 671a33b31c | |||
| 7afc469306 | |||
| 8fd52248b7 | |||
| 69ba2aab11 | |||
| f1df6dcda5 | |||
| 43e16bb913 | |||
| 4147211f94 | |||
| 63e49c5d3c | |||
| 35580c0849 | |||
| 8949a595fe | |||
| bf8ef0a767 | |||
| 39962a3f48 | |||
| 4964621014 | |||
| 18e1a26da1 | |||
| 1d91ca5716 | |||
| 1040646610 | |||
| fcd71931e7 | |||
| bdbb74aff1 | |||
| 6f4029983a | |||
| b2d25b1883 | |||
| ba19d4f043 | |||
| b222fe5afa | |||
| f945defa2b | |||
| 4f0e4bc1ca | |||
| 41abc8404d | |||
| 2b08c4c344 | |||
| 97d91ddddb | |||
| ec30b12fd1 | |||
| 9997fc11b1 | |||
| c6ff0e6492 | |||
| a3220ecae6 | |||
| 218864d08c | |||
| 3d0d70ece6 | |||
| f629731930 | |||
| e7a7b2417b | |||
| 0b24a9abc3 | |||
| ca77b5210f | |||
| 0874f1c350 | |||
| d89b99f42b | |||
| 7bd6ec68a8 | |||
| bfe2eeb833 | |||
| e97ab1fe3c | |||
| b3ee2a8885 | |||
| 80b09e3212 | |||
| 0eb3714abc | |||
| 7991977443 | |||
| 5e5431c9f9 | |||
| 1fc05d1a30 | |||
| 21833e7c31 | |||
| 79daeb23a9 | |||
| 761c2578fb | |||
| 4d3145e559 | |||
| 91e29a3bf1 | |||
| f6a4486c65 | |||
| fc8b512931 | |||
| e5dd15da82 | |||
| e4140d71ab | |||
| 8312780c47 | |||
| 5accc3dec2 | |||
| d875989866 | |||
| 38c92a2338 | |||
| ce76b5db16 | |||
| dfc4889d45 | |||
| 41431282ee | |||
| 5821b2f03c | |||
| 78d2bf736c | |||
| 6c098c3e0a | |||
| bfb140d2e9 | |||
| f71a1a7a89 | |||
| e8aab39620 | |||
| 1d578d8563 | |||
| abfd443541 | |||
| 81cbb6e5cf | |||
| 010c5cab87 | |||
| 415858119a | |||
| 1838a731d6 | |||
| 1e304fad65 | |||
| 999c9b3dc5 | |||
| e15edbd54b | |||
| e5cb77d168 | |||
| cf521d4c7c | |||
| 6f09474193 | |||
| 7626933352 | |||
| 9e1d8c2fc6 | |||
| 6defed2915 | |||
| d729eed7c2 | |||
| f280032dcf | |||
| 7e85137012 | |||
| 88f2c3abd3 | |||
| 1a21e01f85 | |||
| d302e817c8 | |||
| 1e1b0424d7 | |||
| 03f028b7e2 | |||
| b1d35de8e4 | |||
| ea6b9e5260 | |||
| 06d869aaa5 | |||
| 907cebdd6d | |||
| 745902bc7e | |||
| ef0b3c9f9c | |||
| 532c077ddf | |||
| cd905a6593 | |||
| d0bf9d9bfb | |||
| ddc79a631d | |||
| 6015f60db4 | |||
| a6608bd7ea | |||
| fb2d8c6406 | |||
| c84ffb54d2 | |||
| 306bbdc697 | |||
| 9879ecad85 | |||
| f0fcef5744 | |||
| aa8a6058b5 | |||
| 48103bd244 | |||
| 600ac17a5f | |||
| d46f28792c | |||
| 0f7379c941 | |||
| 4317fad798 | |||
| 5cfccb7e1d | |||
| 097eecd78a | |||
| 64b4642c49 | |||
| 0e87d14ca8 | |||
| 4d22b35a9f | |||
| 26586b4514 | |||
| 95fb2a7d7f | |||
| fa66ea31d3 | |||
| e0d3b819e5 | |||
| 17a0b4f3d0 | |||
| d0d228d9f4 | |||
| 309acb961b | |||
| 12f8ebb3ea | |||
| 612861061c | |||
| 83af5ec36b | |||
| 74102d0319 | |||
| fbd05a0fcf | |||
| a53c786fe0 | |||
| eb2728e5b9 | |||
| 3f17223387 | |||
| 74104cf107 | |||
| 13b4879723 | |||
| f1ec0b2c59 | |||
| 6d44daf599 | |||
| 644a6f5569 | |||
| fb83396522 | |||
| e825bd0bdb | |||
| 61823ec7e2 | |||
| cd133cbbe3 | |||
| 0e7a1bb76c | |||
| f86bf69ebc | |||
| adddf330fd | |||
| 10adb57b83 | |||
| 3160fe9abc | |||
| 6adb27d173 | |||
| 6e6aae2ea3 | |||
| 41a140d16c | |||
| 8880ab6498 | |||
| 389becc4f6 | |||
| 923530972a | |||
| b84850df9f | |||
| 9e7dc1d11d | |||
| 2830ed6147 | |||
| bfa919d078 | |||
| f09c28e61f | |||
| bfdba7713e | |||
| d6cadc1e3f | |||
| 20a6a3f195 | |||
| f60de45b52 | |||
| 77031d1ae4 | |||
| 9483a88ee1 | |||
| 3438a4f063 |
+1
-1
@@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_devices.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
|
||||
Generated
+2
-2
@@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/amazon_devices/ @chemelli74
|
||||
/tests/components/amazon_devices/ @chemelli74
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Amazon",
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"amazon_devices",
|
||||
"alexa_devices",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "shelly",
|
||||
"name": "shelly",
|
||||
"integrations": ["shelly"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(TimeoutError):
|
||||
async with timeout(5):
|
||||
hubs: list[aiopulse.Hub] = [
|
||||
hubs = [
|
||||
hub
|
||||
async for hub in aiopulse.Hub.discover()
|
||||
if hub.id not in already_configured
|
||||
|
||||
@@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
try:
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
|
||||
async def _async_update_data(self) -> AirGradientData:
|
||||
try:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Amazon Devices integration."""
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -13,7 +13,7 @@ PLATFORMS = [
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Amazon Devices platform."""
|
||||
"""Set up Alexa Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
|
||||
+5
-2
@@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -25,7 +26,7 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Amazon Devices binary sensor entity description."""
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
@@ -34,10 +35,12 @@ BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
),
|
||||
@@ -49,7 +52,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices binary sensors based on a config entry."""
|
||||
"""Set up Alexa Devices binary sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Amazon Devices integration."""
|
||||
"""Config flow for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Amazon Devices."""
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.positive_int,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
"""Amazon Devices constants."""
|
||||
"""Alexa Devices constants."""
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "amazon_devices"
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Support for Amazon Devices."""
|
||||
"""Support for Alexa Devices."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Base coordinator for Amazon Devices."""
|
||||
"""Base coordinator for Alexa Devices."""
|
||||
|
||||
config_entry: AmazonConfigEntry
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Diagnostics support for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
devices: list[dict[str, dict[str, Any]]] = [
|
||||
build_device_data(device) for device in coordinator.data.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": devices,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
assert device_entry.serial_number
|
||||
|
||||
return build_device_data(coordinator.data[device_entry.serial_number])
|
||||
|
||||
|
||||
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"""Build device data for diagnostics."""
|
||||
return {
|
||||
"account name": device.account_name,
|
||||
"capabilities": device.capabilities,
|
||||
"device family": device.device_family,
|
||||
"device type": device.device_type,
|
||||
"device cluster members": device.device_cluster_members,
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
}
|
||||
+11
-7
@@ -1,4 +1,4 @@
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||
@@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
|
||||
|
||||
|
||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details = coordinator.api.get_model_details(self.device)
|
||||
model = model_details["model"] if model_details else None
|
||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||
model = model_details.get("model")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer="Amazon",
|
||||
hw_version=model_details["hw_version"] if model_details else None,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
@@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._serial_num in self.coordinator.data
|
||||
return (
|
||||
super().available
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"domain": "alexa_devices",
|
||||
"name": "Alexa Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
+2
-2
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Amazon Devices notify entity description."""
|
||||
"""Alexa Devices notify entity description."""
|
||||
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
@@ -49,7 +49,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices notification entity based on a config entry."""
|
||||
"""Set up Alexa Devices notification entity based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
+3
-1
@@ -45,7 +45,9 @@ rules:
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Network information not relevant
|
||||
discovery: done
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
+7
-7
@@ -5,23 +5,23 @@
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password sent to your email address."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::amazon_devices::common::data_country%]",
|
||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::amazon_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::amazon_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::amazon_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
+2
-2
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Amazon Devices switch entity description."""
|
||||
"""Alexa Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
@@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Amazon Devices switches based on a config entry."""
|
||||
"""Set up Alexa Devices switches based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"domain": "amazon_devices",
|
||||
"name": "Amazon Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "EC2BEB*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
"requirements": ["apsystems-ez1==2.7.0"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.48.2"
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||
|
||||
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||
"""Initialise a Bosch Alarm control panel entity."""
|
||||
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||
super().__init__(panel, area_id, unique_id, True, False, True)
|
||||
self._attr_unique_id = self._area_unique_id
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
||||
@@ -21,7 +21,11 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -90,6 +94,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
||||
@@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SOURCE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -17,6 +19,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, entry.entry_id, entry.options[CONF_SOURCE]
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_SOURCE]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The dlib_face_detect component."""
|
||||
|
||||
DOMAIN = "dlib_face_detect"
|
||||
|
||||
@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@@ -25,6 +32,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Detect",
|
||||
},
|
||||
)
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
add_entities(
|
||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""The dlib_face_identify component."""
|
||||
|
||||
CONF_FACES = "faces"
|
||||
DOMAIN = "dlib_face_identify"
|
||||
|
||||
@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_FACES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FACES = "faces"
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -39,6 +45,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Identify",
|
||||
},
|
||||
)
|
||||
|
||||
confidence: float = config[CONF_CONFIDENCE]
|
||||
faces: dict[str, str] = config[CONF_FACES]
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||
"""Validate hostname."""
|
||||
|
||||
async def async_check(
|
||||
hostname: str, resolver: str, qtype: str, port: int = 53
|
||||
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
|
||||
) -> bool:
|
||||
"""Return if able to resolve hostname."""
|
||||
result = False
|
||||
result: bool = False
|
||||
with contextlib.suppress(DNSError):
|
||||
result = bool(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
result: dict[str, bool] = {}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||
}
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,17 +23,18 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -58,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
return
|
||||
|
||||
device_registry.async_update_device(
|
||||
device_id=envoy_device.id,
|
||||
new_connections={connection},
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
},
|
||||
connections={connection},
|
||||
)
|
||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
}
|
||||
|
||||
@@ -226,6 +226,7 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -23,6 +22,7 @@ from aioesphomeapi import (
|
||||
RequiresEncryptionAPIError,
|
||||
UserService,
|
||||
UserServiceArgType,
|
||||
parse_log_message,
|
||||
)
|
||||
from awesomeversion import AwesomeVersion
|
||||
import voluptuous as vol
|
||||
@@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = {
|
||||
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
|
||||
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
|
||||
}
|
||||
# 7-bit and 8-bit C1 ANSI sequences
|
||||
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
||||
ANSI_ESCAPE_78BIT = re.compile(
|
||||
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -387,13 +382,15 @@ class ESPHomeManager:
|
||||
|
||||
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
|
||||
"""Handle a log message from the API."""
|
||||
log: bytes = msg.message
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
|
||||
)
|
||||
for line in parse_log_message(
|
||||
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
|
||||
):
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
line,
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_equivalent_log_level(self) -> LogLevel:
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==32.2.1",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.15.1"
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
|
||||
if self._static_info.supports_pause:
|
||||
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
|
||||
self._attr_supported_features = flags
|
||||
self._entry_data.media_player_formats[static_info.unique_id] = cast(
|
||||
self._entry_data.media_player_formats[self.unique_id] = cast(
|
||||
MediaPlayerInfo, static_info
|
||||
).supported_formats
|
||||
|
||||
@@ -114,9 +114,8 @@ class EsphomeMediaPlayer(
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
|
||||
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
|
||||
|
||||
supported_formats: list[MediaPlayerSupportedFormat] | None = (
|
||||
self._entry_data.media_player_formats.get(self._static_info.unique_id)
|
||||
self._entry_data.media_player_formats.get(self.unique_id)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -139,7 +138,7 @@ class EsphomeMediaPlayer(
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle entity being removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._entry_data.media_player_formats.pop(self.entity_id, None)
|
||||
self._entry_data.media_player_formats.pop(self.unique_id, None)
|
||||
|
||||
def _get_proxy_url(
|
||||
self,
|
||||
|
||||
@@ -84,6 +84,7 @@ async def async_setup_entry(
|
||||
name=f"Freebox {sensor_name}",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
for sensor_name in router.sensors_temperature
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250526.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import aiohttp
|
||||
from aiohttp import web
|
||||
from gassist_text import TextAssistant
|
||||
from google.oauth2.credentials import Credentials
|
||||
from grpc import RpcError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -25,6 +26,7 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -83,7 +85,17 @@ async def async_send_text_commands(
|
||||
) as assistant:
|
||||
command_response_list = []
|
||||
for command in commands:
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
try:
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
except RpcError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to send command '%s' to Google Assistant: %s",
|
||||
command,
|
||||
err,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="grpc_error"
|
||||
) from err
|
||||
text_response = resp[0]
|
||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||
audio_response = resp[2]
|
||||
|
||||
@@ -57,5 +57,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"grpc_error": {
|
||||
"message": "Failed to communicate with Google Assistant"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,12 @@ from .const import (
|
||||
UNITS_IMPERIAL,
|
||||
UNITS_METRIC,
|
||||
)
|
||||
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
|
||||
from .helpers import (
|
||||
InvalidApiKeyException,
|
||||
PermissionDeniedException,
|
||||
UnknownException,
|
||||
validate_config_entry,
|
||||
)
|
||||
|
||||
RECONFIGURE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -188,6 +193,8 @@ async def validate_input(
|
||||
user_input[CONF_ORIGIN],
|
||||
user_input[CONF_DESTINATION],
|
||||
)
|
||||
except PermissionDeniedException:
|
||||
return {"base": "permission_denied"}
|
||||
except InvalidApiKeyException:
|
||||
return {"base": "invalid_auth"}
|
||||
except TimeoutError:
|
||||
|
||||
@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
|
||||
Forbidden,
|
||||
GatewayTimeout,
|
||||
GoogleAPIError,
|
||||
PermissionDenied,
|
||||
Unauthorized,
|
||||
)
|
||||
from google.maps.routing_v2 import (
|
||||
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
|
||||
from google.type import latlng_pb2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||
try:
|
||||
formatted_coordinates = coordinates.split(",")
|
||||
vol.Schema(cv.gps(formatted_coordinates))
|
||||
except (AttributeError, vol.ExactSequenceInvalid):
|
||||
except (AttributeError, vol.Invalid):
|
||||
return Waypoint(address=location)
|
||||
return Waypoint(
|
||||
location=Location(
|
||||
@@ -67,6 +76,9 @@ async def validate_config_entry(
|
||||
await client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
||||
)
|
||||
except PermissionDenied as permission_error:
|
||||
_LOGGER.error("Permission denied: %s", permission_error.message)
|
||||
raise PermissionDeniedException from permission_error
|
||||
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||
raise InvalidApiKeyException from unauthorized_error
|
||||
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
|
||||
|
||||
class UnknownException(Exception):
|
||||
"""Unknown API Error."""
|
||||
|
||||
|
||||
class PermissionDeniedException(Exception):
|
||||
"""Permission Denied Error."""
|
||||
|
||||
|
||||
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Create an issue for the Routes API being disabled."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"routes_api_disabled_{entry.entry_id}",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="routes_api_disabled",
|
||||
translation_placeholders={
|
||||
"entry_title": entry.title,
|
||||
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
|
||||
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Delete the issue for the Routes API being disabled."""
|
||||
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Route,
|
||||
@@ -58,7 +58,11 @@ from .const import (
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||
UNITS_TO_GOOGLE_SDK_ENUM,
|
||||
)
|
||||
from .helpers import convert_to_waypoint
|
||||
from .helpers import (
|
||||
convert_to_waypoint,
|
||||
create_routes_api_disabled_issue,
|
||||
delete_routes_api_disabled_issue,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
response = await self._client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||
)
|
||||
_LOGGER.debug("Received response: %s", response)
|
||||
if response is not None and len(response.routes) > 0:
|
||||
self._route = response.routes[0]
|
||||
delete_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
except PermissionDenied:
|
||||
_LOGGER.error("Routes API is disabled for this API key")
|
||||
create_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
self._route = None
|
||||
except GoogleAPIError as ex:
|
||||
_LOGGER.error("Error getting travel time: %s", ex)
|
||||
self._route = None
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
@@ -100,5 +101,11 @@
|
||||
"fewer_transfers": "Fewer transfers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"routes_api_disabled": {
|
||||
"title": "The Routes API must be enabled",
|
||||
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The gstreamer component."""
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PIPELINE = "pipeline"
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
||||
@@ -48,6 +50,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Gstreamer platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "GStreamer",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
pipeline = config.get(CONF_PIPELINE)
|
||||
|
||||
@@ -37,6 +37,7 @@ from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
discovery_flow,
|
||||
issue_registry as ir,
|
||||
)
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.deprecation import (
|
||||
@@ -51,9 +52,11 @@ from homeassistant.helpers.hassio import (
|
||||
get_supervisor_ip as _get_supervisor_ip,
|
||||
is_hassio as _is_hassio,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service_info.hassio import (
|
||||
HassioServiceInfo as _HassioServiceInfo,
|
||||
)
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
@@ -109,7 +112,7 @@ from .coordinator import (
|
||||
get_core_info, # noqa: F401
|
||||
get_core_stats, # noqa: F401
|
||||
get_host_info, # noqa: F401
|
||||
get_info, # noqa: F401
|
||||
get_info,
|
||||
get_issues_info, # noqa: F401
|
||||
get_os_info,
|
||||
get_supervisor_info, # noqa: F401
|
||||
@@ -168,6 +171,11 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
|
||||
|
||||
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
|
||||
|
||||
DEPRECATION_URL = (
|
||||
"https://www.home-assistant.io/blog/2025/05/22/"
|
||||
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
|
||||
)
|
||||
|
||||
|
||||
def valid_addon(value: Any) -> str:
|
||||
"""Validate value is a valid addon slug."""
|
||||
@@ -546,6 +554,63 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
|
||||
system_info = await async_get_system_info(hass)
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
info = get_info(hass)
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
arch = system_info["arch"]
|
||||
board = os_info.get("board")
|
||||
supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"}
|
||||
if is_haos and arch == "armv7" and supported_board:
|
||||
issue_id = "deprecated_os_"
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board):
|
||||
deprecated_architecture = True
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": "OS" if is_haos else "Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
listener()
|
||||
|
||||
listener = coordinator.async_add_listener(deprecated_setup_issue)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,11 +22,14 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
@@ -56,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the HDDTemp sensor."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "hddtemp",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
@@ -5,26 +5,13 @@ from __future__ import annotations
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
TRAVEL_MODE_PUBLIC,
|
||||
)
|
||||
from .const import TRAVEL_MODE_PUBLIC
|
||||
from .coordinator import (
|
||||
HereConfigEntry,
|
||||
HERERoutingDataUpdateCoordinator,
|
||||
HERETransitDataUpdateCoordinator,
|
||||
)
|
||||
from .model import HERETravelTimeConfig
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -33,29 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry)
|
||||
"""Set up HERE Travel Time from a config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
|
||||
arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, ""))
|
||||
departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, ""))
|
||||
|
||||
here_travel_time_config = HERETravelTimeConfig(
|
||||
destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
|
||||
destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
|
||||
destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
|
||||
origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
|
||||
origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
|
||||
origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=config_entry.options[CONF_ROUTE_MODE],
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
|
||||
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
|
||||
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
|
||||
cls = HERETransitDataUpdateCoordinator
|
||||
else:
|
||||
cls = HERERoutingDataUpdateCoordinator
|
||||
|
||||
data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config)
|
||||
data_coordinator = cls(hass, config_entry, api_key)
|
||||
config_entry.runtime_data = data_coordinator
|
||||
|
||||
async def _async_update_at_start(_: HomeAssistant) -> None:
|
||||
|
||||
@@ -26,7 +26,7 @@ from here_transit import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfLength
|
||||
from homeassistant.const import CONF_MODE, UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
@@ -34,8 +34,21 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
|
||||
from .model import HERETravelTimeConfig, HERETravelTimeData
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTE_MODE_FASTEST,
|
||||
)
|
||||
from .model import HERETravelTimeAPIParams, HERETravelTimeData
|
||||
|
||||
BACKOFF_MULTIPLIER = 1.1
|
||||
|
||||
@@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[
|
||||
|
||||
|
||||
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
|
||||
"""here_routing DataUpdateCoordinator."""
|
||||
"""HERETravelTime DataUpdateCoordinator for the routing API."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERERoutingApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if self.config.route_mode == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
||||
" mode: %s, arrival: %s, departure: %s"
|
||||
),
|
||||
origin,
|
||||
destination,
|
||||
route_mode,
|
||||
TransportMode(self.config.travel_mode),
|
||||
arrival,
|
||||
departure,
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.route_mode,
|
||||
TransportMode(params.travel_mode),
|
||||
params.arrival,
|
||||
params.departure,
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self._api.route(
|
||||
transport_mode=TransportMode(self.config.travel_mode),
|
||||
origin=here_routing.Place(origin[0], origin[1]),
|
||||
destination=here_routing.Place(destination[0], destination[1]),
|
||||
routing_mode=route_mode,
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
transport_mode=TransportMode(params.travel_mode),
|
||||
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
||||
destination=here_routing.Place(
|
||||
params.destination[0], params.destination[1]
|
||||
),
|
||||
routing_mode=params.route_mode,
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
return_values=[Return.POLYINE, Return.SUMMARY],
|
||||
spans=[Spans.NAMES],
|
||||
)
|
||||
@@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
class HERETransitDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[HERETravelTimeData | None]
|
||||
):
|
||||
"""HERETravelTime DataUpdateCoordinator."""
|
||||
"""HERETravelTime DataUpdateCoordinator for the transit API."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator(
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERETransitApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData | None:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
|
||||
" departure: %s"
|
||||
),
|
||||
origin,
|
||||
destination,
|
||||
arrival,
|
||||
departure,
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.arrival,
|
||||
params.departure,
|
||||
)
|
||||
try:
|
||||
response = await self._api.route(
|
||||
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
|
||||
destination=here_transit.Place(
|
||||
latitude=destination[0], longitude=destination[1]
|
||||
origin=here_transit.Place(
|
||||
latitude=params.origin[0], longitude=params.origin[1]
|
||||
),
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
destination=here_transit.Place(
|
||||
latitude=params.destination[0], longitude=params.destination[1]
|
||||
),
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
return_values=[
|
||||
here_transit.Return.POLYLINE,
|
||||
here_transit.Return.TRAVEL_SUMMARY,
|
||||
@@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator(
|
||||
|
||||
def prepare_parameters(
|
||||
hass: HomeAssistant,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> tuple[list[str], list[str], str | None, str | None]:
|
||||
config_entry: HereConfigEntry,
|
||||
) -> HERETravelTimeAPIParams:
|
||||
"""Prepare parameters for the HERE api."""
|
||||
|
||||
def _from_entity_id(entity_id: str) -> list[str]:
|
||||
@@ -305,32 +308,55 @@ def prepare_parameters(
|
||||
return formatted_coordinates
|
||||
|
||||
# Destination
|
||||
if config.destination_entity_id is not None:
|
||||
destination = _from_entity_id(config.destination_entity_id)
|
||||
if (
|
||||
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
|
||||
) is not None:
|
||||
destination = _from_entity_id(str(destination_entity_id))
|
||||
else:
|
||||
destination = [
|
||||
str(config.destination_latitude),
|
||||
str(config.destination_longitude),
|
||||
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
|
||||
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
|
||||
]
|
||||
|
||||
# Origin
|
||||
if config.origin_entity_id is not None:
|
||||
origin = _from_entity_id(config.origin_entity_id)
|
||||
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
|
||||
origin = _from_entity_id(str(origin_entity_id))
|
||||
else:
|
||||
origin = [
|
||||
str(config.origin_latitude),
|
||||
str(config.origin_longitude),
|
||||
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
|
||||
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
|
||||
]
|
||||
|
||||
# Arrival/Departure
|
||||
arrival: str | None = None
|
||||
departure: str | None = None
|
||||
if config.arrival is not None:
|
||||
arrival = next_datetime(config.arrival).isoformat()
|
||||
if config.departure is not None:
|
||||
departure = next_datetime(config.departure).isoformat()
|
||||
arrival: datetime | None = None
|
||||
if (
|
||||
conf_arrival := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_ARRIVAL_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
arrival = next_datetime(conf_arrival)
|
||||
departure: datetime | None = None
|
||||
if (
|
||||
conf_departure := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_DEPARTURE_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
departure = next_datetime(conf_departure)
|
||||
|
||||
return (origin, destination, arrival, departure)
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
|
||||
return HERETravelTimeAPIParams(
|
||||
destination=destination,
|
||||
origin=origin,
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=route_mode,
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
|
||||
|
||||
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from datetime import datetime
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
@@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict):
|
||||
|
||||
|
||||
@dataclass
|
||||
class HERETravelTimeConfig:
|
||||
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
|
||||
class HERETravelTimeAPIParams:
|
||||
"""Configuration for polling the HERE API."""
|
||||
|
||||
destination_latitude: float | None
|
||||
destination_longitude: float | None
|
||||
destination_entity_id: str | None
|
||||
origin_latitude: float | None
|
||||
origin_longitude: float | None
|
||||
origin_entity_id: str | None
|
||||
destination: list[str]
|
||||
origin: list[str]
|
||||
travel_mode: str
|
||||
route_mode: str
|
||||
arrival: time | None
|
||||
departure: time | None
|
||||
arrival: datetime | None
|
||||
departure: datetime | None
|
||||
|
||||
@@ -8,8 +8,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
|
||||
@@ -51,6 +53,30 @@ async def async_setup_entry(
|
||||
entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
# history_stats does not allow replacing the input entity.
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_ENTITY_ID]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ OPTIONS_FLOW = {
|
||||
}
|
||||
|
||||
|
||||
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Handle a config flow for History stats."""
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
|
||||
@@ -25,17 +25,12 @@ def _get_obj_holidays_and_language(
|
||||
selected_categories: list[str] | None,
|
||||
) -> tuple[HolidayBase, str]:
|
||||
"""Get the object for the requested country and year."""
|
||||
if selected_categories is None:
|
||||
categories = [PUBLIC]
|
||||
else:
|
||||
categories = [PUBLIC, *selected_categories]
|
||||
|
||||
obj_holidays = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=language,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
if language == "en":
|
||||
for lang in obj_holidays.supported_languages:
|
||||
@@ -45,7 +40,7 @@ def _get_obj_holidays_and_language(
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=lang,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
language = lang
|
||||
break
|
||||
@@ -59,7 +54,7 @@ def _get_obj_holidays_and_language(
|
||||
subdiv=province,
|
||||
years={dt_util.now().year, dt_util.now().year + 1},
|
||||
language=default_language,
|
||||
categories=categories,
|
||||
categories=selected_categories,
|
||||
)
|
||||
language = default_language
|
||||
|
||||
@@ -77,6 +72,11 @@ async def async_setup_entry(
|
||||
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
|
||||
language = hass.config.language
|
||||
|
||||
if categories is None:
|
||||
categories = [PUBLIC]
|
||||
else:
|
||||
categories = [PUBLIC, *categories]
|
||||
|
||||
obj_holidays, language = await hass.async_add_executor_job(
|
||||
_get_obj_holidays_and_language, country, province, language, categories
|
||||
)
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.73", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -12,3 +12,13 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
|
||||
authorize_url=OAUTH2_AUTHORIZE,
|
||||
token_url=OAUTH2_TOKEN,
|
||||
)
|
||||
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
return {
|
||||
"developer_dashboard_url": "https://developer.home-connect.com/",
|
||||
"applications_url": "https://developer.home-connect.com/applications",
|
||||
"register_application_url": "https://developer.home-connect.com/application/add",
|
||||
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
|
||||
}
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
"macaddress": "C8D778*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|siemens)-*",
|
||||
"hostname": "(balay|bosch|neff|siemens)-*",
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "siemens-*",
|
||||
"hostname": "(siemens|neff)-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"application_credentials": {
|
||||
"description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**."
|
||||
},
|
||||
"common": {
|
||||
"confirmed": "Confirmed",
|
||||
"present": "Present"
|
||||
@@ -13,7 +16,7 @@
|
||||
"description": "The Home Connect integration needs to re-authenticate your account"
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Home Connect device on your network. Press **Submit** to continue setting up Home Connect."
|
||||
"description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -38,7 +38,6 @@ from homeassistant.helpers import (
|
||||
restore_state,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service import (
|
||||
async_extract_config_entry_ids,
|
||||
@@ -402,46 +401,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
info = await async_get_system_info(hass)
|
||||
|
||||
installation_type = info["installation_type"][15:]
|
||||
deprecated_method = installation_type in {
|
||||
"Core",
|
||||
"Supervised",
|
||||
}
|
||||
arch = info["arch"]
|
||||
if arch == "armv7":
|
||||
if installation_type == "OS":
|
||||
# Local import to avoid circular dependencies
|
||||
# We use the import helper because hassio
|
||||
# may not be loaded yet and we don't want to
|
||||
# do blocking I/O in the event loop to import it.
|
||||
if TYPE_CHECKING:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components import hassio
|
||||
else:
|
||||
hassio = await async_import_module(
|
||||
hass, "homeassistant.components.hassio"
|
||||
)
|
||||
os_info = hassio.get_os_info(hass)
|
||||
assert os_info is not None
|
||||
issue_id = "deprecated_os_"
|
||||
board = os_info.get("board")
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
elif installation_type == "Container":
|
||||
if installation_type in {"Core", "Container"}:
|
||||
deprecated_method = installation_type == "Core"
|
||||
arch = info["arch"]
|
||||
if arch == "armv7" and installation_type == "Container":
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
@@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container_armv7",
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (
|
||||
arch == "armv7" and installation_type != "Container"
|
||||
):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
issue_id += "_method"
|
||||
if deprecated_architecture:
|
||||
issue_id += "_architecture"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant."
|
||||
},
|
||||
"historic_currency": {
|
||||
"title": "The configured currency is no longer in use",
|
||||
|
||||
@@ -112,6 +112,7 @@ class HomematicipHAP:
|
||||
self.config_entry = config_entry
|
||||
|
||||
self._ws_close_requested = False
|
||||
self._ws_connection_closed = asyncio.Event()
|
||||
self._retry_task: asyncio.Task | None = None
|
||||
self._tries = 0
|
||||
self._accesspoint_connected = True
|
||||
@@ -218,6 +219,8 @@ class HomematicipHAP:
|
||||
try:
|
||||
await self.home.get_current_state_async()
|
||||
hmip_events = self.home.enable_events()
|
||||
self.home.set_on_connected_handler(self.ws_connected_handler)
|
||||
self.home.set_on_disconnected_handler(self.ws_disconnected_handler)
|
||||
tries = 0
|
||||
await hmip_events
|
||||
except HmipConnectionError:
|
||||
@@ -267,6 +270,18 @@ class HomematicipHAP:
|
||||
"Reset connection to access point id %s", self.config_entry.unique_id
|
||||
)
|
||||
|
||||
async def ws_connected_handler(self) -> None:
|
||||
"""Handle websocket connected."""
|
||||
_LOGGER.debug("WebSocket connection to HomematicIP established")
|
||||
if self._ws_connection_closed.is_set():
|
||||
await self.get_state()
|
||||
self._ws_connection_closed.clear()
|
||||
|
||||
async def ws_disconnected_handler(self) -> None:
|
||||
"""Handle websocket disconnection."""
|
||||
_LOGGER.warning("WebSocket connection to HomematicIP closed")
|
||||
self._ws_connection_closed.set()
|
||||
|
||||
async def get_hap(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
@@ -290,6 +305,7 @@ class HomematicipHAP:
|
||||
raise HmipcConnectionError from err
|
||||
home.on_update(self.async_update)
|
||||
home.on_create(self.async_create_entity)
|
||||
|
||||
hass.loop.create_task(self.async_connect())
|
||||
|
||||
return home
|
||||
|
||||
@@ -4,16 +4,16 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState
|
||||
from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState
|
||||
from homematicip.base.functionalChannels import NotificationLightChannel
|
||||
from homematicip.device import (
|
||||
BrandDimmer,
|
||||
BrandSwitchMeasuring,
|
||||
BrandSwitchNotificationLight,
|
||||
Dimmer,
|
||||
DinRailDimmer3,
|
||||
FullFlushDimmer,
|
||||
PluggableDimmer,
|
||||
SwitchMeasuring,
|
||||
WiredDimmer3,
|
||||
)
|
||||
from packaging.version import Version
|
||||
@@ -44,9 +44,12 @@ async def async_setup_entry(
|
||||
hap = config_entry.runtime_data
|
||||
entities: list[HomematicipGenericEntity] = []
|
||||
for device in hap.home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
if (
|
||||
isinstance(device, SwitchMeasuring)
|
||||
and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING
|
||||
):
|
||||
entities.append(HomematicipLightMeasuring(hap, device))
|
||||
elif isinstance(device, BrandSwitchNotificationLight):
|
||||
if isinstance(device, BrandSwitchNotificationLight):
|
||||
device_version = Version(device.firmwareVersion)
|
||||
entities.append(HomematicipLight(hap, device))
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.1.1"]
|
||||
"requirements": ["homematicip==2.0.4"]
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import (
|
||||
FunctionalChannel,
|
||||
)
|
||||
from homematicip.device import (
|
||||
BrandSwitchMeasuring,
|
||||
EnergySensorsInterface,
|
||||
FloorTerminalBlock6,
|
||||
FloorTerminalBlock10,
|
||||
FloorTerminalBlock12,
|
||||
FullFlushSwitchMeasuring,
|
||||
HeatingThermostat,
|
||||
HeatingThermostatCompact,
|
||||
HeatingThermostatEvo,
|
||||
@@ -26,9 +24,9 @@ from homematicip.device import (
|
||||
MotionDetectorOutdoor,
|
||||
MotionDetectorPushButton,
|
||||
PassageDetector,
|
||||
PlugableSwitchMeasuring,
|
||||
PresenceDetectorIndoor,
|
||||
RoomControlDeviceAnalog,
|
||||
SwitchMeasuring,
|
||||
TemperatureDifferenceSensor2,
|
||||
TemperatureHumiditySensorDisplay,
|
||||
TemperatureHumiditySensorOutdoor,
|
||||
@@ -143,14 +141,7 @@ async def async_setup_entry(
|
||||
),
|
||||
):
|
||||
entities.append(HomematicipIlluminanceSensor(hap, device))
|
||||
if isinstance(
|
||||
device,
|
||||
(
|
||||
PlugableSwitchMeasuring,
|
||||
BrandSwitchMeasuring,
|
||||
FullFlushSwitchMeasuring,
|
||||
),
|
||||
):
|
||||
if isinstance(device, SwitchMeasuring):
|
||||
entities.append(HomematicipPowerSensor(hap, device))
|
||||
entities.append(HomematicipEnergySensor(hap, device))
|
||||
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):
|
||||
|
||||
@@ -4,20 +4,19 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homematicip.base.enums import DeviceType
|
||||
from homematicip.device import (
|
||||
BrandSwitch2,
|
||||
BrandSwitchMeasuring,
|
||||
DinRailSwitch,
|
||||
DinRailSwitch4,
|
||||
FullFlushInputSwitch,
|
||||
FullFlushSwitchMeasuring,
|
||||
HeatingSwitch2,
|
||||
MultiIOBox,
|
||||
OpenCollector8Module,
|
||||
PlugableSwitch,
|
||||
PlugableSwitchMeasuring,
|
||||
PrintedCircuitBoardSwitch2,
|
||||
PrintedCircuitBoardSwitchBattery,
|
||||
SwitchMeasuring,
|
||||
WiredSwitch8,
|
||||
)
|
||||
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
|
||||
@@ -43,12 +42,10 @@ async def async_setup_entry(
|
||||
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
|
||||
]
|
||||
for device in hap.home.devices:
|
||||
if isinstance(device, BrandSwitchMeasuring):
|
||||
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring
|
||||
# This entity is implemented in the light platform and will
|
||||
# not be added in the switch platform
|
||||
pass
|
||||
elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)):
|
||||
if (
|
||||
isinstance(device, SwitchMeasuring)
|
||||
and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING
|
||||
):
|
||||
entities.append(HomematicipSwitchMeasuring(hap, device))
|
||||
elif isinstance(device, WiredSwitch8):
|
||||
entities.extend(
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pydrawise"],
|
||||
"requirements": ["pydrawise==2025.3.0"]
|
||||
"requirements": ["pydrawise==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.6.0"]
|
||||
"requirements": ["aioimmich==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
import mimetypes
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.exceptions import ImmichError
|
||||
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
"""Set up Immich media source."""
|
||||
entries = hass.config_entries.async_entries(
|
||||
DOMAIN, include_disabled=False, include_ignore=False
|
||||
)
|
||||
hass.http.register_view(ImmichMediaView(hass))
|
||||
return ImmichMediaSource(hass, entries)
|
||||
return ImmichMediaSource(hass)
|
||||
|
||||
|
||||
class ImmichMediaSourceIdentifier:
|
||||
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
|
||||
|
||||
def __init__(self, identifier: str) -> None:
|
||||
"""Split identifier into parts."""
|
||||
parts = identifier.split("/")
|
||||
# coonfig_entry.unique_id/album_id/asset_it/filename
|
||||
parts = identifier.split("|")
|
||||
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
|
||||
self.unique_id = parts[0]
|
||||
self.album_id = parts[1] if len(parts) > 1 else None
|
||||
self.asset_id = parts[2] if len(parts) > 2 else None
|
||||
self.file_name = parts[3] if len(parts) > 2 else None
|
||||
self.collection = parts[1] if len(parts) > 1 else None
|
||||
self.collection_id = parts[2] if len(parts) > 2 else None
|
||||
self.asset_id = parts[3] if len(parts) > 3 else None
|
||||
self.file_name = parts[4] if len(parts) > 3 else None
|
||||
self.mime_type = parts[5] if len(parts) > 3 else None
|
||||
|
||||
|
||||
class ImmichMediaSource(MediaSource):
|
||||
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
|
||||
|
||||
name = "Immich"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize Immich media source."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.entries = entries
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
|
||||
raise BrowseError("Immich is not configured")
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
children=[
|
||||
*await self._async_build_immich(item),
|
||||
*await self._async_build_immich(item, entries),
|
||||
],
|
||||
)
|
||||
|
||||
async def _async_build_immich(
|
||||
self, item: MediaSourceItem
|
||||
self, item: MediaSourceItem, entries: list[ConfigEntry]
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Handle browsing different immich instances."""
|
||||
if not item.identifier:
|
||||
LOGGER.debug("Render all Immich instances")
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for entry in self.entries
|
||||
for entry in entries
|
||||
]
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
entry: ImmichConfigEntry | None = (
|
||||
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
|
||||
assert entry
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
if identifier.album_id is None:
|
||||
# Get Albums
|
||||
if identifier.collection is None:
|
||||
LOGGER.debug("Render all collections for %s", entry.title)
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{identifier.unique_id}|albums",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title="albums",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
]
|
||||
|
||||
if identifier.collection_id is None:
|
||||
LOGGER.debug("Render all albums for %s", entry.title)
|
||||
try:
|
||||
albums = await immich_api.albums.async_get_all_albums()
|
||||
except ImmichError:
|
||||
@@ -118,80 +130,85 @@ class ImmichMediaSource(MediaSource):
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{item.identifier}/{album.album_id}",
|
||||
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title=album.name,
|
||||
title=album.album_name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
|
||||
# Request items of album
|
||||
LOGGER.debug(
|
||||
"Render all assets of album %s for %s",
|
||||
identifier.collection_id,
|
||||
entry.title,
|
||||
)
|
||||
try:
|
||||
album_info = await immich_api.albums.async_get_album_info(
|
||||
identifier.album_id
|
||||
identifier.collection_id
|
||||
)
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
ret = [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("image/")
|
||||
]
|
||||
ret: list[BrowseMediaSource] = []
|
||||
for asset in album_info.assets:
|
||||
if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
|
||||
("image/", "video/")
|
||||
):
|
||||
continue
|
||||
|
||||
ret.extend(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
||||
if mime_type.startswith("image/"):
|
||||
media_class = MediaClass.IMAGE
|
||||
can_play = False
|
||||
thumb_mime_type = mime_type
|
||||
else:
|
||||
media_class = MediaClass.VIDEO
|
||||
can_play = True
|
||||
thumb_mime_type = "image/jpeg"
|
||||
|
||||
ret.append(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.original_file_name}|"
|
||||
f"{mime_type}"
|
||||
),
|
||||
media_class=media_class,
|
||||
media_content_type=mime_type,
|
||||
title=asset.original_file_name,
|
||||
can_play=can_play,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}",
|
||||
)
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("video/")
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
if identifier.file_name is None:
|
||||
raise Unresolvable("No file name")
|
||||
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise Unresolvable("No file extension")
|
||||
try:
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
except IndexError as err:
|
||||
raise Unresolvable(
|
||||
f"Could not parse identifier: {item.identifier}"
|
||||
) from err
|
||||
|
||||
if identifier.mime_type is None:
|
||||
raise Unresolvable(
|
||||
f"Could not resolve identifier that has no mime-type: {item.identifier}"
|
||||
)
|
||||
|
||||
return PlayMedia(
|
||||
(
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
|
||||
),
|
||||
mime_type,
|
||||
identifier.mime_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,10 +229,10 @@ class ImmichMediaView(HomeAssistantView):
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
raise HTTPNotFound
|
||||
|
||||
asset_id, file_name, size = location.split("/")
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise HTTPNotFound
|
||||
try:
|
||||
asset_id, size, mime_type_base, mime_type_format = location.split("/")
|
||||
except ValueError as err:
|
||||
raise HTTPNotFound from err
|
||||
|
||||
entry: ImmichConfigEntry | None = (
|
||||
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
@@ -226,7 +243,7 @@ class ImmichMediaView(HomeAssistantView):
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
# stream response for videos
|
||||
if mime_type.startswith("video/"):
|
||||
if mime_type_base == "video":
|
||||
try:
|
||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||
except ImmichError as exc:
|
||||
@@ -243,4 +260,4 @@ class ImmichMediaView(HomeAssistantView):
|
||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||
except ImmichError as exc:
|
||||
raise HTTPNotFound from exc
|
||||
return Response(body=image, content_type=mime_type)
|
||||
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")
|
||||
|
||||
@@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
from .const import CONF_SOURCE_SENSOR
|
||||
|
||||
@@ -21,6 +23,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.options[CONF_SOURCE_SENSOR],
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_SOURCE_SENSOR]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_SOURCE_SENSOR],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.15"]
|
||||
"requirements": ["pyiskra==0.1.21"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
|
||||
coordinator = JellyfinDataUpdateCoordinator(
|
||||
hass, entry, client, server_info, user_id
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.async_on_unload(client.stop)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
|
||||
"""Initialize the Jellyfin entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["hdate"],
|
||||
"requirements": ["hdate[astral]==1.1.0"],
|
||||
"requirements": ["hdate[astral]==1.1.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ async def async_setup_entry(
|
||||
JewishCalendarTimeSensor(config_entry, description)
|
||||
for description in TIME_SENSORS
|
||||
)
|
||||
async_add_entities(sensors)
|
||||
async_add_entities(sensors, update_before_add=True)
|
||||
|
||||
|
||||
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||
@@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Call when entity is added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
await self.async_update_data()
|
||||
|
||||
async def async_update_data(self) -> None:
|
||||
async def async_update(self) -> None:
|
||||
"""Update the state of the sensor."""
|
||||
now = dt_util.now()
|
||||
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
|
||||
|
||||
@@ -11,8 +11,9 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "keyboard"
|
||||
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Listen for keyboard events."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Keyboard",
|
||||
},
|
||||
)
|
||||
|
||||
keyboard = PyKeyboard()
|
||||
keyboard.special_key_assignment()
|
||||
|
||||
@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.6"]
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_on",
|
||||
translation_key="prebrew_time_on",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_off",
|
||||
translation_key="prebrew_time_off",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
|
||||
@@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle Zeroconf discovery."""
|
||||
|
||||
# Do not probe the device if the host is already configured
|
||||
self._async_abort_entries_match({CONF_HOST: discovery_info.host})
|
||||
|
||||
session: ClientSession = await async_get_client_session(self.hass)
|
||||
bridge: LinkPlayBridge | None = None
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.8"],
|
||||
"requirements": ["python-linkplay==0.2.11"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import time
|
||||
import lirc
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the LIRC capability."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "LIRC",
|
||||
},
|
||||
)
|
||||
# blocking=True gives unexpected behavior (multiple responses for 1 press)
|
||||
# also by not blocking, we allow hass to shut down the thread gracefully
|
||||
# on exit.
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -24,3 +24,11 @@ async def async_get_auth_implementation(
|
||||
token_url=OAUTH2_TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
return {
|
||||
"developer_dashboard_url": "https://developer.honeywellhome.com",
|
||||
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"application_credentials": {
|
||||
"description": "To be able to log in to Honeywell Lyric the integration requires a client ID and secret. To acquire those, please follow the following steps.\n\n1. Go to the [Honeywell Lyric Developer Apps Dashboard]({developer_dashboard_url}).\n1. Sign up for a developer account if you don't have one yet. This is a separate account from your Honeywell account.\n1. Log in with your Honeywell Lyric developer account.\n1. Go to the **My Apps** section.\n1. Press the **CREATE NEW APP** button.\n1. Give the application a name of your choice.\n1. Set the **Callback URL** to `{redirect_url}`.\n1. Save your changes.\\n1. Copy the **Consumer Key** and paste it here as the **Client ID**, then copy the **Consumer Secret** and paste it here as the **Client Secret**."
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
@@ -9,7 +12,7 @@
|
||||
"description": "The Lyric integration needs to re-authenticate your account."
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric."
|
||||
"description": "Home Assistant has found a Honeywell Lyric device on your network. Be aware that the setup of the Lyric integration is more complicated than other integrations. Press **Submit** to continue setting up Honeywell Lyric."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
|
||||
# don't discover this entry if the supported state list is empty
|
||||
secondary_value_is_not=[],
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MinPINCodeLength",
|
||||
translation_key="min_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MaxPINCodeLength",
|
||||
translation_key="max_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="TargetPositionLiftPercent100ths",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
||||
@@ -390,12 +390,6 @@
|
||||
"evse_user_max_charge_current": {
|
||||
"name": "User max charge current"
|
||||
},
|
||||
"min_pin_code_length": {
|
||||
"name": "Min PIN code length"
|
||||
},
|
||||
"max_pin_code_length": {
|
||||
"name": "Max PIN code length"
|
||||
},
|
||||
"window_covering_target_position": {
|
||||
"name": "Target opening position"
|
||||
}
|
||||
|
||||
@@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = {
|
||||
116: "custom_program_20",
|
||||
323: "pyrolytic",
|
||||
326: "descale",
|
||||
327: "evaporate_water",
|
||||
335: "shabbat_program",
|
||||
336: "yom_tov",
|
||||
356: "defrost",
|
||||
|
||||
@@ -542,6 +542,7 @@
|
||||
"endive_strips": "Endive (strips)",
|
||||
"espresso": "Espresso",
|
||||
"espresso_macchiato": "Espresso macchiato",
|
||||
"evaporate_water": "Evaporate water",
|
||||
"express": "Express",
|
||||
"express_20": "Express 20'",
|
||||
"extra_quiet": "Extra quiet",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user