Compare commits
197 Commits
dev
...
2025.6.0b9
Author | SHA1 | Date |
---|---|---|
|
dc4627f413 | |
|
02524b8b9b | |
|
60b8230ecc | |
|
75e6f23a82 | |
|
1f221712a2 | |
|
43797c03cc | |
|
89637a618e | |
|
fd605e0abe | |
|
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 |
|
@ -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.*
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
),
|
||||
)
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
|
@ -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"]
|
||||
}
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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%]"
|
||||
}
|
||||
}
|
||||
},
|
|
@ -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.2"]
|
||||
}
|
||||
|
|
|
@ -5,11 +5,18 @@ import voluptuous as vol
|
|||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "generic_hygrostat"
|
||||
|
@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
entry.options[CONF_HUMIDIFIER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HUMIDIFIER: 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(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||
# but not the humidity sensor because the generic_hygrostat adds itself to the
|
||||
# humidifier's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HUMIDIFIER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
"""The generic_thermostat component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
from .const import CONF_HEATER, PLATFORMS
|
||||
from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
@ -17,6 +21,55 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
entry.entry_id,
|
||||
entry.options[CONF_HEATER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HEATER: 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(
|
||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||
# not the temperature sensor because the generic_hygrostat adds itself to the
|
||||
# heater's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HEATER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HEATER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -9,8 +9,10 @@ from functools import partial
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import aiofiles
|
||||
from aiohasupervisor import SupervisorError
|
||||
import voluptuous as vol
|
||||
|
||||
|
@ -37,6 +39,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,6 +54,7 @@ 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,
|
||||
)
|
||||
|
@ -109,7 +113,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 +172,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."""
|
||||
|
@ -225,6 +234,17 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
|||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = await arch_file.read()
|
||||
return {"x86": "i386"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
class APIEndpointSettings(NamedTuple):
|
||||
"""Settings for API endpoint."""
|
||||
|
||||
|
@ -546,6 +566,62 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
|
||||
arch = await _get_arch()
|
||||
|
||||
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
|
||||
board = os_info.get("board")
|
||||
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
|
||||
unsupported_os_on_board = board in {"rpi3", "rpi4"}
|
||||
if is_haos and (unsupported_board or unsupported_os_on_board):
|
||||
issue_id = "deprecated_os_"
|
||||
if unsupported_os_on_board:
|
||||
issue_id += "aarch64"
|
||||
elif unsupported_board:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
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/",
|
||||
},
|
||||
)
|
||||
bit32 = _is_32_bit()
|
||||
deprecated_architecture = bit32 and not (
|
||||
unsupported_board or unsupported_os_on_board
|
||||
)
|
||||
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,
|
||||
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,8 +4,10 @@ import asyncio
|
|||
from collections.abc import Callable, Coroutine
|
||||
import itertools as it
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config as conf_util, core_config
|
||||
|
@ -38,7 +40,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,
|
||||
|
@ -95,6 +96,17 @@ DEPRECATION_URL = (
|
|||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = (await arch_file.read()).strip()
|
||||
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
||||
|
@ -402,79 +414,42 @@ 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"
|
||||
if installation_type in {"Core", "Container"}:
|
||||
deprecated_method = installation_type == "Core"
|
||||
bit32 = _is_32_bit()
|
||||
arch = info["arch"]
|
||||
if bit32 and installation_type == "Container":
|
||||
arch = await _get_arch()
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_container",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container",
|
||||
translation_placeholders={"arch": arch},
|
||||
)
|
||||
deprecated_architecture = bit32 and installation_type != "Container"
|
||||
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_guide": "https://www.home-assistant.io/installation/",
|
||||
"installation_type": installation_type,
|
||||
"arch": arch,
|
||||
},
|
||||
)
|
||||
elif installation_type == "Container":
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_container_armv7",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
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,
|
||||
},
|
||||
)
|
||||
|
||||
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",
|
||||
|
@ -103,9 +107,9 @@
|
|||
"title": "Deprecation notice: 32-bit architecture",
|
||||
"description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_container_armv7": {
|
||||
"deprecated_container": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
"description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_os_aarch64": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue