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.alarm_control_panel.*
|
||||||
homeassistant.components.alert.*
|
homeassistant.components.alert.*
|
||||||
homeassistant.components.alexa.*
|
homeassistant.components.alexa.*
|
||||||
|
homeassistant.components.alexa_devices.*
|
||||||
homeassistant.components.alpha_vantage.*
|
homeassistant.components.alpha_vantage.*
|
||||||
homeassistant.components.amazon_devices.*
|
|
||||||
homeassistant.components.amazon_polly.*
|
homeassistant.components.amazon_polly.*
|
||||||
homeassistant.components.amberelectric.*
|
homeassistant.components.amberelectric.*
|
||||||
homeassistant.components.ambient_network.*
|
homeassistant.components.ambient_network.*
|
||||||
|
|
|
@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
|
||||||
/tests/components/alert/ @home-assistant/core @frenck
|
/tests/components/alert/ @home-assistant/core @frenck
|
||||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||||
/homeassistant/components/amazon_devices/ @chemelli74
|
/homeassistant/components/alexa_devices/ @chemelli74
|
||||||
/tests/components/amazon_devices/ @chemelli74
|
/tests/components/alexa_devices/ @chemelli74
|
||||||
/homeassistant/components/amazon_polly/ @jschlyter
|
/homeassistant/components/amazon_polly/ @jschlyter
|
||||||
/homeassistant/components/amberelectric/ @madpilot
|
/homeassistant/components/amberelectric/ @madpilot
|
||||||
/tests/components/amberelectric/ @madpilot
|
/tests/components/amberelectric/ @madpilot
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"name": "Amazon",
|
"name": "Amazon",
|
||||||
"integrations": [
|
"integrations": [
|
||||||
"alexa",
|
"alexa",
|
||||||
"amazon_devices",
|
"alexa_devices",
|
||||||
"amazon_polly",
|
"amazon_polly",
|
||||||
"aws",
|
"aws",
|
||||||
"aws_s3",
|
"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()
|
entry.unique_id for entry in self._async_current_entries()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hubs: list[aiopulse.Hub] = []
|
||||||
with suppress(TimeoutError):
|
with suppress(TimeoutError):
|
||||||
async with timeout(5):
|
async with timeout(5):
|
||||||
hubs: list[aiopulse.Hub] = [
|
hubs = [
|
||||||
hub
|
hub
|
||||||
async for hub in aiopulse.Hub.discover()
|
async for hub in aiopulse.Hub.discover()
|
||||||
if hub.id not in already_configured
|
if hub.id not in already_configured
|
||||||
|
|
|
@ -51,9 +51,16 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||||
|
|
||||||
async def _async_setup(self) -> None:
|
async def _async_setup(self) -> None:
|
||||||
"""Set up the coordinator."""
|
"""Set up the coordinator."""
|
||||||
self._current_version = (
|
try:
|
||||||
await self.client.get_current_measures()
|
self._current_version = (
|
||||||
).firmware_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:
|
async def _async_update_data(self) -> AirGradientData:
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Amazon Devices integration."""
|
"""Alexa Devices integration."""
|
||||||
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -13,7 +13,7 @@ PLATFORMS = [
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||||
"""Set up Amazon Devices platform."""
|
"""Set up Alexa Devices platform."""
|
||||||
|
|
||||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorEntity,
|
BinarySensorEntity,
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
@ -25,7 +26,7 @@ PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||||
"""Amazon Devices binary sensor entity description."""
|
"""Alexa Devices binary sensor entity description."""
|
||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice], bool]
|
||||||
|
|
||||||
|
@ -34,10 +35,12 @@ BINARY_SENSORS: Final = (
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="online",
|
key="online",
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
is_on_fn=lambda _device: _device.online,
|
is_on_fn=lambda _device: _device.online,
|
||||||
),
|
),
|
||||||
AmazonBinarySensorEntityDescription(
|
AmazonBinarySensorEntityDescription(
|
||||||
key="bluetooth",
|
key="bluetooth",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
translation_key="bluetooth",
|
translation_key="bluetooth",
|
||||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||||
),
|
),
|
||||||
|
@ -49,7 +52,7 @@ async def async_setup_entry(
|
||||||
entry: AmazonConfigEntry,
|
entry: AmazonConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> 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
|
coordinator = entry.runtime_data
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
"""Config flow for Amazon Devices integration."""
|
"""Config flow for Alexa Devices integration."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=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(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
): CountrySelector(),
|
): CountrySelector(),
|
||||||
vol.Required(CONF_USERNAME): cv.string,
|
vol.Required(CONF_USERNAME): cv.string,
|
||||||
vol.Required(CONF_PASSWORD): 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
|
import logging
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__package__)
|
_LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
DOMAIN = "amazon_devices"
|
DOMAIN = "alexa_devices"
|
||||||
CONF_LOGIN_DATA = "login_data"
|
CONF_LOGIN_DATA = "login_data"
|
|
@ -1,4 +1,4 @@
|
||||||
"""Support for Amazon Devices."""
|
"""Support for Alexa Devices."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||||
"""Base coordinator for Amazon Devices."""
|
"""Base coordinator for Alexa Devices."""
|
||||||
|
|
||||||
config_entry: AmazonConfigEntry
|
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.api import AmazonDevice
|
||||||
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||||
|
@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
|
||||||
|
|
||||||
|
|
||||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||||
"""Defines a base Amazon Devices entity."""
|
"""Defines a base Alexa Devices entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
@ -25,15 +25,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||||
"""Initialize the entity."""
|
"""Initialize the entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._serial_num = serial_num
|
self._serial_num = serial_num
|
||||||
model_details = coordinator.api.get_model_details(self.device)
|
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||||
model = model_details["model"] if model_details else None
|
model = model_details.get("model")
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, serial_num)},
|
identifiers={(DOMAIN, serial_num)},
|
||||||
name=self.device.account_name,
|
name=self.device.account_name,
|
||||||
model=model,
|
model=model,
|
||||||
model_id=self.device.device_type,
|
model_id=self.device.device_type,
|
||||||
manufacturer="Amazon",
|
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||||
hw_version=model_details["hw_version"] if model_details else None,
|
hw_version=model_details.get("hw_version"),
|
||||||
sw_version=(
|
sw_version=(
|
||||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||||
),
|
),
|
||||||
|
@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""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)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||||
"""Amazon Devices notify entity description."""
|
"""Alexa Devices notify entity description."""
|
||||||
|
|
||||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||||
subkey: str
|
subkey: str
|
||||||
|
@ -49,7 +49,7 @@ async def async_setup_entry(
|
||||||
entry: AmazonConfigEntry,
|
entry: AmazonConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> 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
|
coordinator = entry.runtime_data
|
||||||
|
|
|
@ -45,7 +45,9 @@ rules:
|
||||||
discovery-update-info:
|
discovery-update-info:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: Network information not relevant
|
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-data-update: todo
|
||||||
docs-examples: todo
|
docs-examples: todo
|
||||||
docs-known-limitations: todo
|
docs-known-limitations: todo
|
|
@ -5,23 +5,23 @@
|
||||||
"data_description_country": "The country of your Amazon account.",
|
"data_description_country": "The country of your Amazon account.",
|
||||||
"data_description_username": "The email address 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_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": {
|
"config": {
|
||||||
"flow_title": "{username}",
|
"flow_title": "{username}",
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
"data": {
|
"data": {
|
||||||
"country": "[%key:component::amazon_devices::common::data_country%]",
|
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||||
"username": "[%key:common::config_flow::data::username%]",
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
"password": "[%key:common::config_flow::data::password%]",
|
"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": {
|
"data_description": {
|
||||||
"country": "[%key:component::amazon_devices::common::data_description_country%]",
|
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||||
"username": "[%key:component::amazon_devices::common::data_description_username%]",
|
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||||
"password": "[%key:component::amazon_devices::common::data_description_password%]",
|
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
|
@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||||
|
|
||||||
@dataclass(frozen=True, kw_only=True)
|
@dataclass(frozen=True, kw_only=True)
|
||||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||||
"""Amazon Devices switch entity description."""
|
"""Alexa Devices switch entity description."""
|
||||||
|
|
||||||
is_on_fn: Callable[[AmazonDevice], bool]
|
is_on_fn: Callable[[AmazonDevice], bool]
|
||||||
subkey: str
|
subkey: str
|
||||||
|
@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||||
entry: AmazonConfigEntry,
|
entry: AmazonConfigEntry,
|
||||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
) -> None:
|
) -> 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
|
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",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"loggers": ["pyaprilaire"],
|
"loggers": ["pyaprilaire"],
|
||||||
"requirements": ["pyaprilaire==0.9.0"]
|
"requirements": ["pyaprilaire==0.9.1"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["APsystemsEZ1"],
|
"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-auto-recovery==1.5.2",
|
||||||
"bluetooth-data-tools==1.28.1",
|
"bluetooth-data-tools==1.28.1",
|
||||||
"dbus-fast==2.43.0",
|
"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:
|
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||||
"""Initialise a Bosch Alarm control panel entity."""
|
"""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
|
self._attr_unique_id = self._area_unique_id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["caldav", "vobject"],
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"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."""
|
"""The decora component."""
|
||||||
|
|
||||||
|
DOMAIN = "decora"
|
||||||
|
|
|
@ -21,7 +21,11 @@ from homeassistant.components.light import (
|
||||||
LightEntity,
|
LightEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
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 import config_validation as cv
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
@ -90,6 +94,21 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up an Decora switch."""
|
"""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 = []
|
lights = []
|
||||||
for address, device_config in config[CONF_DEVICES].items():
|
for address, device_config in config[CONF_DEVICES].items():
|
||||||
device = {}
|
device = {}
|
||||||
|
|
|
@ -6,8 +6,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_SOURCE, Platform
|
from homeassistant.const import CONF_SOURCE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device import (
|
from homeassistant.helpers.device import (
|
||||||
|
async_entity_id_to_device_id,
|
||||||
async_remove_stale_devices_links_keep_entity_device,
|
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:
|
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]
|
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,))
|
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The dlib_face_detect component."""
|
"""The dlib_face_detect component."""
|
||||||
|
|
||||||
|
DOMAIN = "dlib_face_detect"
|
||||||
|
|
|
@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
|
||||||
ImageProcessingFaceEntity,
|
ImageProcessingFaceEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
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.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +32,20 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Dlib Face detection platform."""
|
"""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]
|
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||||
add_entities(
|
add_entities(
|
||||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
"""The dlib_face_identify component."""
|
"""The dlib_face_identify component."""
|
||||||
|
|
||||||
|
CONF_FACES = "faces"
|
||||||
|
DOMAIN = "dlib_face_identify"
|
||||||
|
|
|
@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
|
||||||
ImageProcessingFaceEntity,
|
ImageProcessingFaceEntity,
|
||||||
)
|
)
|
||||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
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 import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import CONF_FACES, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_FACES = "faces"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
|
@ -39,6 +45,21 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Dlib Face detection platform."""
|
"""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]
|
confidence: float = config[CONF_CONFIDENCE]
|
||||||
faces: dict[str, str] = config[CONF_FACES]
|
faces: dict[str, str] = config[CONF_FACES]
|
||||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import contextlib
|
import contextlib
|
||||||
from typing import Any
|
from typing import Any, Literal
|
||||||
|
|
||||||
import aiodns
|
import aiodns
|
||||||
from aiodns.error import DNSError
|
from aiodns.error import DNSError
|
||||||
|
@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||||
"""Validate hostname."""
|
"""Validate hostname."""
|
||||||
|
|
||||||
async def async_check(
|
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:
|
) -> bool:
|
||||||
"""Return if able to resolve hostname."""
|
"""Return if able to resolve hostname."""
|
||||||
result = False
|
result: bool = False
|
||||||
with contextlib.suppress(DNSError):
|
with contextlib.suppress(DNSError):
|
||||||
result = bool(
|
_resolver = aiodns.DNSResolver(
|
||||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
|
||||||
).query(hostname, qtype)
|
|
||||||
)
|
)
|
||||||
|
result = bool(await _resolver.query(hostname, qtype))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
result: dict[str, bool] = {}
|
result: dict[str, bool] = {}
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
|
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
"""The eddystone_temperature component."""
|
"""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,
|
STATE_UNKNOWN,
|
||||||
UnitOfTemperature,
|
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 import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_BEACONS = "beacons"
|
|
||||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||||
CONF_INSTANCE = "instance"
|
|
||||||
CONF_NAMESPACE = "namespace"
|
|
||||||
|
|
||||||
BEACON_SCHEMA = vol.Schema(
|
BEACON_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -58,6 +59,21 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate configuration, create devices and start monitoring thread."""
|
"""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]
|
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||||
|
|
||||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||||
|
|
|
@ -180,9 +180,15 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
device_registry.async_update_device(
|
device_registry.async_get_or_create(
|
||||||
device_id=envoy_device.id,
|
config_entry_id=self.config_entry.entry_id,
|
||||||
new_connections={connection},
|
identifiers={
|
||||||
|
(
|
||||||
|
DOMAIN,
|
||||||
|
self.envoy_serial_number,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
connections={connection},
|
||||||
)
|
)
|
||||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["env_canada"],
|
"loggers": ["env_canada"],
|
||||||
"requirements": ["env-canada==0.10.2"]
|
"requirements": ["env-canada==0.11.2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,5 +22,5 @@
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["eq3btsmart"],
|
"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
|
_static_info: _InfoT
|
||||||
_state: _StateT
|
_state: _StateT
|
||||||
_has_state: bool
|
_has_state: bool
|
||||||
|
unique_id: str
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||||
|
|
||||||
from aioesphomeapi import (
|
from aioesphomeapi import (
|
||||||
|
@ -23,6 +22,7 @@ from aioesphomeapi import (
|
||||||
RequiresEncryptionAPIError,
|
RequiresEncryptionAPIError,
|
||||||
UserService,
|
UserService,
|
||||||
UserServiceArgType,
|
UserServiceArgType,
|
||||||
|
parse_log_message,
|
||||||
)
|
)
|
||||||
from awesomeversion import AwesomeVersion
|
from awesomeversion import AwesomeVersion
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -110,11 +110,6 @@ LOGGER_TO_LOG_LEVEL = {
|
||||||
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
|
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
|
||||||
logging.CRITICAL: 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
|
@callback
|
||||||
|
@ -387,13 +382,15 @@ class ESPHomeManager:
|
||||||
|
|
||||||
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
|
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
|
||||||
"""Handle a log message from the API."""
|
"""Handle a log message from the API."""
|
||||||
log: bytes = msg.message
|
for line in parse_log_message(
|
||||||
_LOGGER.log(
|
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
|
||||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
):
|
||||||
"%s: %s",
|
_LOGGER.log(
|
||||||
self.entry.title,
|
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||||
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
|
"%s: %s",
|
||||||
)
|
self.entry.title,
|
||||||
|
line,
|
||||||
|
)
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _async_get_equivalent_log_level(self) -> LogLevel:
|
def _async_get_equivalent_log_level(self) -> LogLevel:
|
||||||
|
|
|
@ -17,9 +17,9 @@
|
||||||
"mqtt": ["esphome/discover/#"],
|
"mqtt": ["esphome/discover/#"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==31.1.0",
|
"aioesphomeapi==32.2.1",
|
||||||
"esphome-dashboard-api==1.3.0",
|
"esphome-dashboard-api==1.3.0",
|
||||||
"bleak-esphome==2.15.1"
|
"bleak-esphome==2.16.0"
|
||||||
],
|
],
|
||||||
"zeroconf": ["_esphomelib._tcp.local."]
|
"zeroconf": ["_esphomelib._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
|
||||||
if self._static_info.supports_pause:
|
if self._static_info.supports_pause:
|
||||||
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
|
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
|
||||||
self._attr_supported_features = flags
|
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
|
MediaPlayerInfo, static_info
|
||||||
).supported_formats
|
).supported_formats
|
||||||
|
|
||||||
|
@ -114,9 +114,8 @@ class EsphomeMediaPlayer(
|
||||||
media_id = async_process_play_media_url(self.hass, media_id)
|
media_id = async_process_play_media_url(self.hass, media_id)
|
||||||
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
|
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
|
||||||
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
|
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
|
||||||
|
|
||||||
supported_formats: list[MediaPlayerSupportedFormat] | None = (
|
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 (
|
if (
|
||||||
|
@ -139,7 +138,7 @@ class EsphomeMediaPlayer(
|
||||||
async def async_will_remove_from_hass(self) -> None:
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
"""Handle entity being removed."""
|
"""Handle entity being removed."""
|
||||||
await super().async_will_remove_from_hass()
|
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(
|
def _get_proxy_url(
|
||||||
self,
|
self,
|
||||||
|
|
|
@ -84,6 +84,7 @@ async def async_setup_entry(
|
||||||
name=f"Freebox {sensor_name}",
|
name=f"Freebox {sensor_name}",
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for sensor_name in router.sensors_temperature
|
for sensor_name in router.sensors_temperature
|
||||||
|
|
|
@ -20,5 +20,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["home-assistant-frontend==20250526.0"]
|
"requirements": ["home-assistant-frontend==20250531.2"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,11 +5,18 @@ import voluptuous as vol
|
||||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import Event, HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv, discovery
|
from homeassistant.helpers import (
|
||||||
|
config_validation as cv,
|
||||||
|
discovery,
|
||||||
|
entity_registry as er,
|
||||||
|
)
|
||||||
from homeassistant.helpers.device import (
|
from homeassistant.helpers.device import (
|
||||||
|
async_entity_id_to_device_id,
|
||||||
async_remove_stale_devices_links_keep_entity_device,
|
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
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
DOMAIN = "generic_hygrostat"
|
DOMAIN = "generic_hygrostat"
|
||||||
|
@ -88,6 +95,54 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
entry.options[CONF_HUMIDIFIER],
|
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,))
|
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
"""The generic_thermostat component."""
|
"""The generic_thermostat component."""
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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 (
|
from homeassistant.helpers.device import (
|
||||||
|
async_entity_id_to_device_id,
|
||||||
async_remove_stale_devices_links_keep_entity_device,
|
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:
|
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.entry_id,
|
||||||
entry.options[CONF_HEATER],
|
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
"integration_type": "system",
|
"integration_type": "system",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"quality_scale": "internal",
|
"quality_scale": "internal",
|
||||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
"requirements": ["go2rtc-client==0.2.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["googleapiclient"],
|
"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 aiohttp import web
|
||||||
from gassist_text import TextAssistant
|
from gassist_text import TextAssistant
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
|
from grpc import RpcError
|
||||||
|
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.media_player import (
|
from homeassistant.components.media_player import (
|
||||||
|
@ -25,6 +26,7 @@ from homeassistant.components.media_player import (
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||||
from homeassistant.helpers.event import async_call_later
|
from homeassistant.helpers.event import async_call_later
|
||||||
|
|
||||||
|
@ -83,7 +85,17 @@ async def async_send_text_commands(
|
||||||
) as assistant:
|
) as assistant:
|
||||||
command_response_list = []
|
command_response_list = []
|
||||||
for command in commands:
|
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]
|
text_response = resp[0]
|
||||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||||
audio_response = resp[2]
|
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_IMPERIAL,
|
||||||
UNITS_METRIC,
|
UNITS_METRIC,
|
||||||
)
|
)
|
||||||
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
|
from .helpers import (
|
||||||
|
InvalidApiKeyException,
|
||||||
|
PermissionDeniedException,
|
||||||
|
UnknownException,
|
||||||
|
validate_config_entry,
|
||||||
|
)
|
||||||
|
|
||||||
RECONFIGURE_SCHEMA = vol.Schema(
|
RECONFIGURE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
|
@ -188,6 +193,8 @@ async def validate_input(
|
||||||
user_input[CONF_ORIGIN],
|
user_input[CONF_ORIGIN],
|
||||||
user_input[CONF_DESTINATION],
|
user_input[CONF_DESTINATION],
|
||||||
)
|
)
|
||||||
|
except PermissionDeniedException:
|
||||||
|
return {"base": "permission_denied"}
|
||||||
except InvalidApiKeyException:
|
except InvalidApiKeyException:
|
||||||
return {"base": "invalid_auth"}
|
return {"base": "invalid_auth"}
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
|
|
|
@ -7,6 +7,7 @@ from google.api_core.exceptions import (
|
||||||
Forbidden,
|
Forbidden,
|
||||||
GatewayTimeout,
|
GatewayTimeout,
|
||||||
GoogleAPIError,
|
GoogleAPIError,
|
||||||
|
PermissionDenied,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
)
|
)
|
||||||
from google.maps.routing_v2 import (
|
from google.maps.routing_v2 import (
|
||||||
|
@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
|
||||||
from google.type import latlng_pb2
|
from google.type import latlng_pb2
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
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 homeassistant.helpers.location import find_coordinates
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||||
try:
|
try:
|
||||||
formatted_coordinates = coordinates.split(",")
|
formatted_coordinates = coordinates.split(",")
|
||||||
vol.Schema(cv.gps(formatted_coordinates))
|
vol.Schema(cv.gps(formatted_coordinates))
|
||||||
except (AttributeError, vol.ExactSequenceInvalid):
|
except (AttributeError, vol.Invalid):
|
||||||
return Waypoint(address=location)
|
return Waypoint(address=location)
|
||||||
return Waypoint(
|
return Waypoint(
|
||||||
location=Location(
|
location=Location(
|
||||||
|
@ -67,6 +76,9 @@ async def validate_config_entry(
|
||||||
await client.compute_routes(
|
await client.compute_routes(
|
||||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
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:
|
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||||
raise InvalidApiKeyException from unauthorized_error
|
raise InvalidApiKeyException from unauthorized_error
|
||||||
|
@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
|
||||||
|
|
||||||
class UnknownException(Exception):
|
class UnknownException(Exception):
|
||||||
"""Unknown API Error."""
|
"""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 typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from google.api_core.client_options import ClientOptions
|
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 (
|
from google.maps.routing_v2 import (
|
||||||
ComputeRoutesRequest,
|
ComputeRoutesRequest,
|
||||||
Route,
|
Route,
|
||||||
|
@ -58,7 +58,11 @@ from .const import (
|
||||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||||
UNITS_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__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||||
response = await self._client.compute_routes(
|
response = await self._client.compute_routes(
|
||||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||||
)
|
)
|
||||||
|
_LOGGER.debug("Received response: %s", response)
|
||||||
if response is not None and len(response.routes) > 0:
|
if response is not None and len(response.routes) > 0:
|
||||||
self._route = 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:
|
except GoogleAPIError as ex:
|
||||||
_LOGGER.error("Error getting travel time: %s", ex)
|
_LOGGER.error("Error getting travel time: %s", ex)
|
||||||
self._route = None
|
self._route = None
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"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%]",
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||||
|
@ -100,5 +101,11 @@
|
||||||
"fewer_transfers": "Fewer transfers"
|
"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."""
|
"""The gstreamer component."""
|
||||||
|
|
||||||
|
DOMAIN = "gstreamer"
|
||||||
|
|
|
@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
|
||||||
async_process_play_media_url,
|
async_process_play_media_url,
|
||||||
)
|
)
|
||||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
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 import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONF_PIPELINE = "pipeline"
|
CONF_PIPELINE = "pipeline"
|
||||||
|
|
||||||
DOMAIN = "gstreamer"
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
||||||
|
@ -48,6 +50,20 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Gstreamer platform."""
|
"""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)
|
name = config.get(CONF_NAME)
|
||||||
pipeline = config.get(CONF_PIPELINE)
|
pipeline = config.get(CONF_PIPELINE)
|
||||||
|
|
|
@ -9,8 +9,10 @@ from functools import partial
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import struct
|
||||||
from typing import Any, NamedTuple
|
from typing import Any, NamedTuple
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
from aiohasupervisor import SupervisorError
|
from aiohasupervisor import SupervisorError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -37,6 +39,7 @@ from homeassistant.helpers import (
|
||||||
config_validation as cv,
|
config_validation as cv,
|
||||||
device_registry as dr,
|
device_registry as dr,
|
||||||
discovery_flow,
|
discovery_flow,
|
||||||
|
issue_registry as ir,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.deprecation import (
|
from homeassistant.helpers.deprecation import (
|
||||||
|
@ -51,6 +54,7 @@ from homeassistant.helpers.hassio import (
|
||||||
get_supervisor_ip as _get_supervisor_ip,
|
get_supervisor_ip as _get_supervisor_ip,
|
||||||
is_hassio as _is_hassio,
|
is_hassio as _is_hassio,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||||
from homeassistant.helpers.service_info.hassio import (
|
from homeassistant.helpers.service_info.hassio import (
|
||||||
HassioServiceInfo as _HassioServiceInfo,
|
HassioServiceInfo as _HassioServiceInfo,
|
||||||
)
|
)
|
||||||
|
@ -109,7 +113,7 @@ from .coordinator import (
|
||||||
get_core_info, # noqa: F401
|
get_core_info, # noqa: F401
|
||||||
get_core_stats, # noqa: F401
|
get_core_stats, # noqa: F401
|
||||||
get_host_info, # noqa: F401
|
get_host_info, # noqa: F401
|
||||||
get_info, # noqa: F401
|
get_info,
|
||||||
get_issues_info, # noqa: F401
|
get_issues_info, # noqa: F401
|
||||||
get_os_info,
|
get_os_info,
|
||||||
get_supervisor_info, # noqa: F401
|
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]+$"))
|
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:
|
def valid_addon(value: Any) -> str:
|
||||||
"""Validate value is a valid addon slug."""
|
"""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):
|
class APIEndpointSettings(NamedTuple):
|
||||||
"""Settings for API endpoint."""
|
"""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()
|
await coordinator.async_config_entry_first_refresh()
|
||||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The hddtemp component."""
|
"""The hddtemp component."""
|
||||||
|
|
||||||
|
DOMAIN = "hddtemp"
|
||||||
|
|
|
@ -22,11 +22,14 @@ from homeassistant.const import (
|
||||||
CONF_PORT,
|
CONF_PORT,
|
||||||
UnitOfTemperature,
|
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 import config_validation as cv
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
|
from . import DOMAIN
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_DEVICE = "device"
|
ATTR_DEVICE = "device"
|
||||||
|
@ -56,6 +59,21 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HDDTemp sensor."""
|
"""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)
|
name = config.get(CONF_NAME)
|
||||||
host = config.get(CONF_HOST)
|
host = config.get(CONF_HOST)
|
||||||
port = config.get(CONF_PORT)
|
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.const import CONF_API_KEY, CONF_MODE, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.start import async_at_started
|
from homeassistant.helpers.start import async_at_started
|
||||||
from homeassistant.util import dt as dt_util
|
|
||||||
|
|
||||||
from .const import (
|
from .const import TRAVEL_MODE_PUBLIC
|
||||||
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 .coordinator import (
|
from .coordinator import (
|
||||||
HereConfigEntry,
|
HereConfigEntry,
|
||||||
HERERoutingDataUpdateCoordinator,
|
HERERoutingDataUpdateCoordinator,
|
||||||
HERETransitDataUpdateCoordinator,
|
HERETransitDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
from .model import HERETravelTimeConfig
|
|
||||||
|
|
||||||
PLATFORMS = [Platform.SENSOR]
|
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."""
|
"""Set up HERE Travel Time from a config entry."""
|
||||||
api_key = config_entry.data[CONF_API_KEY]
|
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]
|
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
|
||||||
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
|
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
|
||||||
cls = HERETransitDataUpdateCoordinator
|
cls = HERETransitDataUpdateCoordinator
|
||||||
else:
|
else:
|
||||||
cls = HERERoutingDataUpdateCoordinator
|
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
|
config_entry.runtime_data = data_coordinator
|
||||||
|
|
||||||
async def _async_update_at_start(_: HomeAssistant) -> None:
|
async def _async_update_at_start(_: HomeAssistant) -> None:
|
||||||
|
|
|
@ -26,7 +26,7 @@ from here_transit import (
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.location import find_coordinates
|
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 import dt as dt_util
|
||||||
from homeassistant.util.unit_conversion import DistanceConverter
|
from homeassistant.util.unit_conversion import DistanceConverter
|
||||||
|
|
||||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
|
from .const import (
|
||||||
from .model import HERETravelTimeConfig, HERETravelTimeData
|
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
|
BACKOFF_MULTIPLIER = 1.1
|
||||||
|
|
||||||
|
@ -47,7 +60,7 @@ type HereConfigEntry = ConfigEntry[
|
||||||
|
|
||||||
|
|
||||||
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
|
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
|
||||||
"""here_routing DataUpdateCoordinator."""
|
"""HERETravelTime DataUpdateCoordinator for the routing API."""
|
||||||
|
|
||||||
config_entry: HereConfigEntry
|
config_entry: HereConfigEntry
|
||||||
|
|
||||||
|
@ -56,7 +69,6 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: HereConfigEntry,
|
config_entry: HereConfigEntry,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
config: HERETravelTimeConfig,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -67,41 +79,34 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
self._api = HERERoutingApi(api_key)
|
self._api = HERERoutingApi(api_key)
|
||||||
self.config = config
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> HERETravelTimeData:
|
async def _async_update_data(self) -> HERETravelTimeData:
|
||||||
"""Get the latest data from the HERE Routing API."""
|
"""Get the latest data from the HERE Routing API."""
|
||||||
origin, destination, arrival, departure = prepare_parameters(
|
params = prepare_parameters(self.hass, self.config_entry)
|
||||||
self.hass, self.config
|
|
||||||
)
|
|
||||||
|
|
||||||
route_mode = (
|
|
||||||
RoutingMode.FAST
|
|
||||||
if self.config.route_mode == ROUTE_MODE_FASTEST
|
|
||||||
else RoutingMode.SHORT
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
(
|
(
|
||||||
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
||||||
" mode: %s, arrival: %s, departure: %s"
|
" mode: %s, arrival: %s, departure: %s"
|
||||||
),
|
),
|
||||||
origin,
|
params.origin,
|
||||||
destination,
|
params.destination,
|
||||||
route_mode,
|
params.route_mode,
|
||||||
TransportMode(self.config.travel_mode),
|
TransportMode(params.travel_mode),
|
||||||
arrival,
|
params.arrival,
|
||||||
departure,
|
params.departure,
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = await self._api.route(
|
response = await self._api.route(
|
||||||
transport_mode=TransportMode(self.config.travel_mode),
|
transport_mode=TransportMode(params.travel_mode),
|
||||||
origin=here_routing.Place(origin[0], origin[1]),
|
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
||||||
destination=here_routing.Place(destination[0], destination[1]),
|
destination=here_routing.Place(
|
||||||
routing_mode=route_mode,
|
params.destination[0], params.destination[1]
|
||||||
arrival_time=arrival,
|
),
|
||||||
departure_time=departure,
|
routing_mode=params.route_mode,
|
||||||
|
arrival_time=params.arrival,
|
||||||
|
departure_time=params.departure,
|
||||||
return_values=[Return.POLYINE, Return.SUMMARY],
|
return_values=[Return.POLYINE, Return.SUMMARY],
|
||||||
spans=[Spans.NAMES],
|
spans=[Spans.NAMES],
|
||||||
)
|
)
|
||||||
|
@ -175,7 +180,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||||
class HERETransitDataUpdateCoordinator(
|
class HERETransitDataUpdateCoordinator(
|
||||||
DataUpdateCoordinator[HERETravelTimeData | None]
|
DataUpdateCoordinator[HERETravelTimeData | None]
|
||||||
):
|
):
|
||||||
"""HERETravelTime DataUpdateCoordinator."""
|
"""HERETravelTime DataUpdateCoordinator for the transit API."""
|
||||||
|
|
||||||
config_entry: HereConfigEntry
|
config_entry: HereConfigEntry
|
||||||
|
|
||||||
|
@ -184,7 +189,6 @@ class HERETransitDataUpdateCoordinator(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config_entry: HereConfigEntry,
|
config_entry: HereConfigEntry,
|
||||||
api_key: str,
|
api_key: str,
|
||||||
config: HERETravelTimeConfig,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
@ -195,32 +199,31 @@ class HERETransitDataUpdateCoordinator(
|
||||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||||
)
|
)
|
||||||
self._api = HERETransitApi(api_key)
|
self._api = HERETransitApi(api_key)
|
||||||
self.config = config
|
|
||||||
|
|
||||||
async def _async_update_data(self) -> HERETravelTimeData | None:
|
async def _async_update_data(self) -> HERETravelTimeData | None:
|
||||||
"""Get the latest data from the HERE Routing API."""
|
"""Get the latest data from the HERE Routing API."""
|
||||||
origin, destination, arrival, departure = prepare_parameters(
|
params = prepare_parameters(self.hass, self.config_entry)
|
||||||
self.hass, self.config
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
(
|
(
|
||||||
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
|
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
|
||||||
" departure: %s"
|
" departure: %s"
|
||||||
),
|
),
|
||||||
origin,
|
params.origin,
|
||||||
destination,
|
params.destination,
|
||||||
arrival,
|
params.arrival,
|
||||||
departure,
|
params.departure,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
response = await self._api.route(
|
response = await self._api.route(
|
||||||
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
|
origin=here_transit.Place(
|
||||||
destination=here_transit.Place(
|
latitude=params.origin[0], longitude=params.origin[1]
|
||||||
latitude=destination[0], longitude=destination[1]
|
|
||||||
),
|
),
|
||||||
arrival_time=arrival,
|
destination=here_transit.Place(
|
||||||
departure_time=departure,
|
latitude=params.destination[0], longitude=params.destination[1]
|
||||||
|
),
|
||||||
|
arrival_time=params.arrival,
|
||||||
|
departure_time=params.departure,
|
||||||
return_values=[
|
return_values=[
|
||||||
here_transit.Return.POLYLINE,
|
here_transit.Return.POLYLINE,
|
||||||
here_transit.Return.TRAVEL_SUMMARY,
|
here_transit.Return.TRAVEL_SUMMARY,
|
||||||
|
@ -285,8 +288,8 @@ class HERETransitDataUpdateCoordinator(
|
||||||
|
|
||||||
def prepare_parameters(
|
def prepare_parameters(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: HERETravelTimeConfig,
|
config_entry: HereConfigEntry,
|
||||||
) -> tuple[list[str], list[str], str | None, str | None]:
|
) -> HERETravelTimeAPIParams:
|
||||||
"""Prepare parameters for the HERE api."""
|
"""Prepare parameters for the HERE api."""
|
||||||
|
|
||||||
def _from_entity_id(entity_id: str) -> list[str]:
|
def _from_entity_id(entity_id: str) -> list[str]:
|
||||||
|
@ -305,32 +308,55 @@ def prepare_parameters(
|
||||||
return formatted_coordinates
|
return formatted_coordinates
|
||||||
|
|
||||||
# Destination
|
# Destination
|
||||||
if config.destination_entity_id is not None:
|
if (
|
||||||
destination = _from_entity_id(config.destination_entity_id)
|
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
|
||||||
|
) is not None:
|
||||||
|
destination = _from_entity_id(str(destination_entity_id))
|
||||||
else:
|
else:
|
||||||
destination = [
|
destination = [
|
||||||
str(config.destination_latitude),
|
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
|
||||||
str(config.destination_longitude),
|
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Origin
|
# Origin
|
||||||
if config.origin_entity_id is not None:
|
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
|
||||||
origin = _from_entity_id(config.origin_entity_id)
|
origin = _from_entity_id(str(origin_entity_id))
|
||||||
else:
|
else:
|
||||||
origin = [
|
origin = [
|
||||||
str(config.origin_latitude),
|
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
|
||||||
str(config.origin_longitude),
|
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
|
||||||
]
|
]
|
||||||
|
|
||||||
# Arrival/Departure
|
# Arrival/Departure
|
||||||
arrival: str | None = None
|
arrival: datetime | None = None
|
||||||
departure: str | None = None
|
if (
|
||||||
if config.arrival is not None:
|
conf_arrival := dt_util.parse_time(
|
||||||
arrival = next_datetime(config.arrival).isoformat()
|
config_entry.options.get(CONF_ARRIVAL_TIME, "")
|
||||||
if config.departure is not None:
|
)
|
||||||
departure = next_datetime(config.departure).isoformat()
|
) 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:
|
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import time
|
from datetime import datetime
|
||||||
from typing import TypedDict
|
from typing import TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,16 +21,12 @@ class HERETravelTimeData(TypedDict):
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HERETravelTimeConfig:
|
class HERETravelTimeAPIParams:
|
||||||
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
|
"""Configuration for polling the HERE API."""
|
||||||
|
|
||||||
destination_latitude: float | None
|
destination: list[str]
|
||||||
destination_longitude: float | None
|
origin: list[str]
|
||||||
destination_entity_id: str | None
|
|
||||||
origin_latitude: float | None
|
|
||||||
origin_longitude: float | None
|
|
||||||
origin_entity_id: str | None
|
|
||||||
travel_mode: str
|
travel_mode: str
|
||||||
route_mode: str
|
route_mode: str
|
||||||
arrival: time | None
|
arrival: datetime | None
|
||||||
departure: time | 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.const import CONF_ENTITY_ID, CONF_STATE
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device import (
|
from homeassistant.helpers.device import (
|
||||||
|
async_entity_id_to_device_id,
|
||||||
async_remove_stale_devices_links_keep_entity_device,
|
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 homeassistant.helpers.template import Template
|
||||||
|
|
||||||
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
|
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
|
||||||
|
@ -51,6 +53,30 @@ async def async_setup_entry(
|
||||||
entry.options[CONF_ENTITY_ID],
|
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
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."""
|
"""Handle a config flow for History stats."""
|
||||||
|
|
||||||
config_flow = CONFIG_FLOW
|
config_flow = CONFIG_FLOW
|
||||||
|
|
|
@ -25,17 +25,12 @@ def _get_obj_holidays_and_language(
|
||||||
selected_categories: list[str] | None,
|
selected_categories: list[str] | None,
|
||||||
) -> tuple[HolidayBase, str]:
|
) -> tuple[HolidayBase, str]:
|
||||||
"""Get the object for the requested country and year."""
|
"""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(
|
obj_holidays = country_holidays(
|
||||||
country,
|
country,
|
||||||
subdiv=province,
|
subdiv=province,
|
||||||
years={dt_util.now().year, dt_util.now().year + 1},
|
years={dt_util.now().year, dt_util.now().year + 1},
|
||||||
language=language,
|
language=language,
|
||||||
categories=categories,
|
categories=selected_categories,
|
||||||
)
|
)
|
||||||
if language == "en":
|
if language == "en":
|
||||||
for lang in obj_holidays.supported_languages:
|
for lang in obj_holidays.supported_languages:
|
||||||
|
@ -45,7 +40,7 @@ def _get_obj_holidays_and_language(
|
||||||
subdiv=province,
|
subdiv=province,
|
||||||
years={dt_util.now().year, dt_util.now().year + 1},
|
years={dt_util.now().year, dt_util.now().year + 1},
|
||||||
language=lang,
|
language=lang,
|
||||||
categories=categories,
|
categories=selected_categories,
|
||||||
)
|
)
|
||||||
language = lang
|
language = lang
|
||||||
break
|
break
|
||||||
|
@ -59,7 +54,7 @@ def _get_obj_holidays_and_language(
|
||||||
subdiv=province,
|
subdiv=province,
|
||||||
years={dt_util.now().year, dt_util.now().year + 1},
|
years={dt_util.now().year, dt_util.now().year + 1},
|
||||||
language=default_language,
|
language=default_language,
|
||||||
categories=categories,
|
categories=selected_categories,
|
||||||
)
|
)
|
||||||
language = default_language
|
language = default_language
|
||||||
|
|
||||||
|
@ -77,6 +72,11 @@ async def async_setup_entry(
|
||||||
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
|
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
|
||||||
language = hass.config.language
|
language = hass.config.language
|
||||||
|
|
||||||
|
if categories is None:
|
||||||
|
categories = [PUBLIC]
|
||||||
|
else:
|
||||||
|
categories = [PUBLIC, *categories]
|
||||||
|
|
||||||
obj_holidays, language = await hass.async_add_executor_job(
|
obj_holidays, language = await hass.async_add_executor_job(
|
||||||
_get_obj_holidays_and_language, country, province, language, categories
|
_get_obj_holidays_and_language, country, province, language, categories
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"requirements": ["holidays==0.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,
|
authorize_url=OAUTH2_AUTHORIZE,
|
||||||
token_url=OAUTH2_TOKEN,
|
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*"
|
"macaddress": "C8D778*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostname": "(bosch|siemens)-*",
|
"hostname": "(balay|bosch|neff|siemens)-*",
|
||||||
"macaddress": "68A40E*"
|
"macaddress": "68A40E*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"hostname": "siemens-*",
|
"hostname": "(siemens|neff)-*",
|
||||||
"macaddress": "38B4D3*"
|
"macaddress": "38B4D3*"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["aiohomeconnect"],
|
"loggers": ["aiohomeconnect"],
|
||||||
"requirements": ["aiohomeconnect==0.17.0"],
|
"requirements": ["aiohomeconnect==0.17.1"],
|
||||||
"zeroconf": ["_homeconnect._tcp.local."]
|
"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": {
|
"common": {
|
||||||
"confirmed": "Confirmed",
|
"confirmed": "Confirmed",
|
||||||
"present": "Present"
|
"present": "Present"
|
||||||
|
@ -13,7 +16,7 @@
|
||||||
"description": "The Home Connect integration needs to re-authenticate your account"
|
"description": "The Home Connect integration needs to re-authenticate your account"
|
||||||
},
|
},
|
||||||
"oauth_discovery": {
|
"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": {
|
"abort": {
|
||||||
|
|
|
@ -4,8 +4,10 @@ import asyncio
|
||||||
from collections.abc import Callable, Coroutine
|
from collections.abc import Callable, Coroutine
|
||||||
import itertools as it
|
import itertools as it
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
import struct
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config as conf_util, core_config
|
from homeassistant import config as conf_util, core_config
|
||||||
|
@ -38,7 +40,6 @@ from homeassistant.helpers import (
|
||||||
restore_state,
|
restore_state,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entity_component import async_update_entity
|
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.issue_registry import IssueSeverity
|
||||||
from homeassistant.helpers.service import (
|
from homeassistant.helpers.service import (
|
||||||
async_extract_config_entry_ids,
|
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
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||||
"""Set up general services related to Home Assistant."""
|
"""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)
|
info = await async_get_system_info(hass)
|
||||||
|
|
||||||
installation_type = info["installation_type"][15:]
|
installation_type = info["installation_type"][15:]
|
||||||
deprecated_method = installation_type in {
|
if installation_type in {"Core", "Container"}:
|
||||||
"Core",
|
deprecated_method = installation_type == "Core"
|
||||||
"Supervised",
|
bit32 = _is_32_bit()
|
||||||
}
|
arch = info["arch"]
|
||||||
arch = info["arch"]
|
if bit32 and installation_type == "Container":
|
||||||
if arch == "armv7":
|
arch = await _get_arch()
|
||||||
if installation_type == "OS":
|
ir.async_create_issue(
|
||||||
# Local import to avoid circular dependencies
|
hass,
|
||||||
# We use the import helper because hassio
|
DOMAIN,
|
||||||
# may not be loaded yet and we don't want to
|
"deprecated_container",
|
||||||
# do blocking I/O in the event loop to import it.
|
learn_more_url=DEPRECATION_URL,
|
||||||
if TYPE_CHECKING:
|
is_fixable=False,
|
||||||
# pylint: disable-next=import-outside-toplevel
|
severity=IssueSeverity.WARNING,
|
||||||
from homeassistant.components import hassio
|
translation_key="deprecated_container",
|
||||||
else:
|
translation_placeholders={"arch": arch},
|
||||||
hassio = await async_import_module(
|
)
|
||||||
hass, "homeassistant.components.hassio"
|
deprecated_architecture = bit32 and installation_type != "Container"
|
||||||
)
|
if deprecated_method or deprecated_architecture:
|
||||||
os_info = hassio.get_os_info(hass)
|
issue_id = "deprecated"
|
||||||
assert os_info is not None
|
if deprecated_method:
|
||||||
issue_id = "deprecated_os_"
|
issue_id += "_method"
|
||||||
board = os_info.get("board")
|
if deprecated_architecture:
|
||||||
if board in {"rpi3", "rpi4"}:
|
issue_id += "_architecture"
|
||||||
issue_id += "aarch64"
|
|
||||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
|
||||||
issue_id += "armv7"
|
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
hass,
|
hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
issue_id,
|
issue_id,
|
||||||
breaks_in_ha_version="2025.12.0",
|
|
||||||
learn_more_url=DEPRECATION_URL,
|
learn_more_url=DEPRECATION_URL,
|
||||||
is_fixable=False,
|
is_fixable=False,
|
||||||
severity=IssueSeverity.WARNING,
|
severity=IssueSeverity.WARNING,
|
||||||
translation_key=issue_id,
|
translation_key=issue_id,
|
||||||
translation_placeholders={
|
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
|
return True
|
||||||
|
|
||||||
|
|
|
@ -18,9 +18,13 @@
|
||||||
"title": "The {integration_title} YAML configuration is being removed",
|
"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."
|
"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": {
|
"deprecated_system_packages_yaml_integration": {
|
||||||
"title": "The {integration_title} integration is being removed",
|
"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": {
|
"historic_currency": {
|
||||||
"title": "The configured currency is no longer in use",
|
"title": "The configured currency is no longer in use",
|
||||||
|
@ -103,9 +107,9 @@
|
||||||
"title": "Deprecation notice: 32-bit architecture",
|
"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."
|
"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%]",
|
"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": {
|
"deprecated_os_aarch64": {
|
||||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||||
|
|
|
@ -112,6 +112,7 @@ class HomematicipHAP:
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
|
||||||
self._ws_close_requested = False
|
self._ws_close_requested = False
|
||||||
|
self._ws_connection_closed = asyncio.Event()
|
||||||
self._retry_task: asyncio.Task | None = None
|
self._retry_task: asyncio.Task | None = None
|
||||||
self._tries = 0
|
self._tries = 0
|
||||||
self._accesspoint_connected = True
|
self._accesspoint_connected = True
|
||||||
|
@ -218,6 +219,8 @@ class HomematicipHAP:
|
||||||
try:
|
try:
|
||||||
await self.home.get_current_state_async()
|
await self.home.get_current_state_async()
|
||||||
hmip_events = self.home.enable_events()
|
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
|
tries = 0
|
||||||
await hmip_events
|
await hmip_events
|
||||||
except HmipConnectionError:
|
except HmipConnectionError:
|
||||||
|
@ -267,6 +270,18 @@ class HomematicipHAP:
|
||||||
"Reset connection to access point id %s", self.config_entry.unique_id
|
"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(
|
async def get_hap(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
@ -290,6 +305,7 @@ class HomematicipHAP:
|
||||||
raise HmipcConnectionError from err
|
raise HmipcConnectionError from err
|
||||||
home.on_update(self.async_update)
|
home.on_update(self.async_update)
|
||||||
home.on_create(self.async_create_entity)
|
home.on_create(self.async_create_entity)
|
||||||
|
|
||||||
hass.loop.create_task(self.async_connect())
|
hass.loop.create_task(self.async_connect())
|
||||||
|
|
||||||
return home
|
return home
|
||||||
|
|
|
@ -4,16 +4,16 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
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.base.functionalChannels import NotificationLightChannel
|
||||||
from homematicip.device import (
|
from homematicip.device import (
|
||||||
BrandDimmer,
|
BrandDimmer,
|
||||||
BrandSwitchMeasuring,
|
|
||||||
BrandSwitchNotificationLight,
|
BrandSwitchNotificationLight,
|
||||||
Dimmer,
|
Dimmer,
|
||||||
DinRailDimmer3,
|
DinRailDimmer3,
|
||||||
FullFlushDimmer,
|
FullFlushDimmer,
|
||||||
PluggableDimmer,
|
PluggableDimmer,
|
||||||
|
SwitchMeasuring,
|
||||||
WiredDimmer3,
|
WiredDimmer3,
|
||||||
)
|
)
|
||||||
from packaging.version import Version
|
from packaging.version import Version
|
||||||
|
@ -44,9 +44,12 @@ async def async_setup_entry(
|
||||||
hap = config_entry.runtime_data
|
hap = config_entry.runtime_data
|
||||||
entities: list[HomematicipGenericEntity] = []
|
entities: list[HomematicipGenericEntity] = []
|
||||||
for device in hap.home.devices:
|
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))
|
entities.append(HomematicipLightMeasuring(hap, device))
|
||||||
elif isinstance(device, BrandSwitchNotificationLight):
|
if isinstance(device, BrandSwitchNotificationLight):
|
||||||
device_version = Version(device.firmwareVersion)
|
device_version = Version(device.firmwareVersion)
|
||||||
entities.append(HomematicipLight(hap, device))
|
entities.append(HomematicipLight(hap, device))
|
||||||
|
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["homematicip"],
|
"loggers": ["homematicip"],
|
||||||
"requirements": ["homematicip==2.0.1.1"]
|
"requirements": ["homematicip==2.0.4"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,12 +11,10 @@ from homematicip.base.functionalChannels import (
|
||||||
FunctionalChannel,
|
FunctionalChannel,
|
||||||
)
|
)
|
||||||
from homematicip.device import (
|
from homematicip.device import (
|
||||||
BrandSwitchMeasuring,
|
|
||||||
EnergySensorsInterface,
|
EnergySensorsInterface,
|
||||||
FloorTerminalBlock6,
|
FloorTerminalBlock6,
|
||||||
FloorTerminalBlock10,
|
FloorTerminalBlock10,
|
||||||
FloorTerminalBlock12,
|
FloorTerminalBlock12,
|
||||||
FullFlushSwitchMeasuring,
|
|
||||||
HeatingThermostat,
|
HeatingThermostat,
|
||||||
HeatingThermostatCompact,
|
HeatingThermostatCompact,
|
||||||
HeatingThermostatEvo,
|
HeatingThermostatEvo,
|
||||||
|
@ -26,9 +24,9 @@ from homematicip.device import (
|
||||||
MotionDetectorOutdoor,
|
MotionDetectorOutdoor,
|
||||||
MotionDetectorPushButton,
|
MotionDetectorPushButton,
|
||||||
PassageDetector,
|
PassageDetector,
|
||||||
PlugableSwitchMeasuring,
|
|
||||||
PresenceDetectorIndoor,
|
PresenceDetectorIndoor,
|
||||||
RoomControlDeviceAnalog,
|
RoomControlDeviceAnalog,
|
||||||
|
SwitchMeasuring,
|
||||||
TemperatureDifferenceSensor2,
|
TemperatureDifferenceSensor2,
|
||||||
TemperatureHumiditySensorDisplay,
|
TemperatureHumiditySensorDisplay,
|
||||||
TemperatureHumiditySensorOutdoor,
|
TemperatureHumiditySensorOutdoor,
|
||||||
|
@ -143,14 +141,7 @@ async def async_setup_entry(
|
||||||
),
|
),
|
||||||
):
|
):
|
||||||
entities.append(HomematicipIlluminanceSensor(hap, device))
|
entities.append(HomematicipIlluminanceSensor(hap, device))
|
||||||
if isinstance(
|
if isinstance(device, SwitchMeasuring):
|
||||||
device,
|
|
||||||
(
|
|
||||||
PlugableSwitchMeasuring,
|
|
||||||
BrandSwitchMeasuring,
|
|
||||||
FullFlushSwitchMeasuring,
|
|
||||||
),
|
|
||||||
):
|
|
||||||
entities.append(HomematicipPowerSensor(hap, device))
|
entities.append(HomematicipPowerSensor(hap, device))
|
||||||
entities.append(HomematicipEnergySensor(hap, device))
|
entities.append(HomematicipEnergySensor(hap, device))
|
||||||
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):
|
if isinstance(device, (WeatherSensor, WeatherSensorPlus, WeatherSensorPro)):
|
||||||
|
|
|
@ -4,20 +4,19 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from homematicip.base.enums import DeviceType
|
||||||
from homematicip.device import (
|
from homematicip.device import (
|
||||||
BrandSwitch2,
|
BrandSwitch2,
|
||||||
BrandSwitchMeasuring,
|
|
||||||
DinRailSwitch,
|
DinRailSwitch,
|
||||||
DinRailSwitch4,
|
DinRailSwitch4,
|
||||||
FullFlushInputSwitch,
|
FullFlushInputSwitch,
|
||||||
FullFlushSwitchMeasuring,
|
|
||||||
HeatingSwitch2,
|
HeatingSwitch2,
|
||||||
MultiIOBox,
|
MultiIOBox,
|
||||||
OpenCollector8Module,
|
OpenCollector8Module,
|
||||||
PlugableSwitch,
|
PlugableSwitch,
|
||||||
PlugableSwitchMeasuring,
|
|
||||||
PrintedCircuitBoardSwitch2,
|
PrintedCircuitBoardSwitch2,
|
||||||
PrintedCircuitBoardSwitchBattery,
|
PrintedCircuitBoardSwitchBattery,
|
||||||
|
SwitchMeasuring,
|
||||||
WiredSwitch8,
|
WiredSwitch8,
|
||||||
)
|
)
|
||||||
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
|
from homematicip.group import ExtendedLinkedSwitchingGroup, SwitchingGroup
|
||||||
|
@ -43,12 +42,10 @@ async def async_setup_entry(
|
||||||
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
|
if isinstance(group, (ExtendedLinkedSwitchingGroup, SwitchingGroup))
|
||||||
]
|
]
|
||||||
for device in hap.home.devices:
|
for device in hap.home.devices:
|
||||||
if isinstance(device, BrandSwitchMeasuring):
|
if (
|
||||||
# BrandSwitchMeasuring inherits PlugableSwitchMeasuring
|
isinstance(device, SwitchMeasuring)
|
||||||
# This entity is implemented in the light platform and will
|
and getattr(device, "deviceType", None) != DeviceType.BRAND_SWITCH_MEASURING
|
||||||
# not be added in the switch platform
|
):
|
||||||
pass
|
|
||||||
elif isinstance(device, (PlugableSwitchMeasuring, FullFlushSwitchMeasuring)):
|
|
||||||
entities.append(HomematicipSwitchMeasuring(hap, device))
|
entities.append(HomematicipSwitchMeasuring(hap, device))
|
||||||
elif isinstance(device, WiredSwitch8):
|
elif isinstance(device, WiredSwitch8):
|
||||||
entities.extend(
|
entities.extend(
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pydrawise"],
|
"loggers": ["pydrawise"],
|
||||||
"requirements": ["pydrawise==2025.3.0"]
|
"requirements": ["pydrawise==2025.6.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["aioimmich"],
|
"loggers": ["aioimmich"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"requirements": ["aioimmich==0.6.0"]
|
"requirements": ["aioimmich==0.9.1"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
import mimetypes
|
|
||||||
|
|
||||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||||
from aioimmich.exceptions import ImmichError
|
from aioimmich.exceptions import ImmichError
|
||||||
|
@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
|
||||||
|
|
||||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||||
"""Set up Immich media source."""
|
"""Set up Immich media source."""
|
||||||
entries = hass.config_entries.async_entries(
|
|
||||||
DOMAIN, include_disabled=False, include_ignore=False
|
|
||||||
)
|
|
||||||
hass.http.register_view(ImmichMediaView(hass))
|
hass.http.register_view(ImmichMediaView(hass))
|
||||||
return ImmichMediaSource(hass, entries)
|
return ImmichMediaSource(hass)
|
||||||
|
|
||||||
|
|
||||||
class ImmichMediaSourceIdentifier:
|
class ImmichMediaSourceIdentifier:
|
||||||
|
@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
|
||||||
|
|
||||||
def __init__(self, identifier: str) -> None:
|
def __init__(self, identifier: str) -> None:
|
||||||
"""Split identifier into parts."""
|
"""Split identifier into parts."""
|
||||||
parts = identifier.split("/")
|
parts = identifier.split("|")
|
||||||
# coonfig_entry.unique_id/album_id/asset_it/filename
|
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
|
||||||
self.unique_id = parts[0]
|
self.unique_id = parts[0]
|
||||||
self.album_id = parts[1] if len(parts) > 1 else None
|
self.collection = parts[1] if len(parts) > 1 else None
|
||||||
self.asset_id = parts[2] if len(parts) > 2 else None
|
self.collection_id = parts[2] if len(parts) > 2 else None
|
||||||
self.file_name = parts[3] 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):
|
class ImmichMediaSource(MediaSource):
|
||||||
|
@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
|
||||||
|
|
||||||
name = "Immich"
|
name = "Immich"
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
|
def __init__(self, hass: HomeAssistant) -> None:
|
||||||
"""Initialize Immich media source."""
|
"""Initialize Immich media source."""
|
||||||
super().__init__(DOMAIN)
|
super().__init__(DOMAIN)
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.entries = entries
|
|
||||||
|
|
||||||
async def async_browse_media(
|
async def async_browse_media(
|
||||||
self,
|
self,
|
||||||
item: MediaSourceItem,
|
item: MediaSourceItem,
|
||||||
) -> BrowseMediaSource:
|
) -> BrowseMediaSource:
|
||||||
"""Return media."""
|
"""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")
|
raise BrowseError("Immich is not configured")
|
||||||
return BrowseMediaSource(
|
return BrowseMediaSource(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
|
@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
|
||||||
can_expand=True,
|
can_expand=True,
|
||||||
children_media_class=MediaClass.DIRECTORY,
|
children_media_class=MediaClass.DIRECTORY,
|
||||||
children=[
|
children=[
|
||||||
*await self._async_build_immich(item),
|
*await self._async_build_immich(item, entries),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _async_build_immich(
|
async def _async_build_immich(
|
||||||
self, item: MediaSourceItem
|
self, item: MediaSourceItem, entries: list[ConfigEntry]
|
||||||
) -> list[BrowseMediaSource]:
|
) -> list[BrowseMediaSource]:
|
||||||
"""Handle browsing different immich instances."""
|
"""Handle browsing different immich instances."""
|
||||||
if not item.identifier:
|
if not item.identifier:
|
||||||
|
LOGGER.debug("Render all Immich instances")
|
||||||
return [
|
return [
|
||||||
BrowseMediaSource(
|
BrowseMediaSource(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
|
@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
|
||||||
can_play=False,
|
can_play=False,
|
||||||
can_expand=True,
|
can_expand=True,
|
||||||
)
|
)
|
||||||
for entry in self.entries
|
for entry in entries
|
||||||
]
|
]
|
||||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||||
entry: ImmichConfigEntry | None = (
|
entry: ImmichConfigEntry | None = (
|
||||||
|
@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
|
||||||
assert entry
|
assert entry
|
||||||
immich_api = entry.runtime_data.api
|
immich_api = entry.runtime_data.api
|
||||||
|
|
||||||
if identifier.album_id is None:
|
if identifier.collection is None:
|
||||||
# Get Albums
|
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:
|
try:
|
||||||
albums = await immich_api.albums.async_get_all_albums()
|
albums = await immich_api.albums.async_get_all_albums()
|
||||||
except ImmichError:
|
except ImmichError:
|
||||||
|
@ -118,80 +130,85 @@ class ImmichMediaSource(MediaSource):
|
||||||
return [
|
return [
|
||||||
BrowseMediaSource(
|
BrowseMediaSource(
|
||||||
domain=DOMAIN,
|
domain=DOMAIN,
|
||||||
identifier=f"{item.identifier}/{album.album_id}",
|
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||||
media_class=MediaClass.DIRECTORY,
|
media_class=MediaClass.DIRECTORY,
|
||||||
media_content_type=MediaClass.IMAGE,
|
media_content_type=MediaClass.IMAGE,
|
||||||
title=album.name,
|
title=album.album_name,
|
||||||
can_play=False,
|
can_play=False,
|
||||||
can_expand=True,
|
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
|
for album in albums
|
||||||
]
|
]
|
||||||
|
|
||||||
# Request items of album
|
LOGGER.debug(
|
||||||
|
"Render all assets of album %s for %s",
|
||||||
|
identifier.collection_id,
|
||||||
|
entry.title,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
album_info = await immich_api.albums.async_get_album_info(
|
album_info = await immich_api.albums.async_get_album_info(
|
||||||
identifier.album_id
|
identifier.collection_id
|
||||||
)
|
)
|
||||||
except ImmichError:
|
except ImmichError:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
ret = [
|
ret: list[BrowseMediaSource] = []
|
||||||
BrowseMediaSource(
|
for asset in album_info.assets:
|
||||||
domain=DOMAIN,
|
if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
|
||||||
identifier=(
|
("image/", "video/")
|
||||||
f"{identifier.unique_id}/"
|
):
|
||||||
f"{identifier.album_id}/"
|
continue
|
||||||
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.extend(
|
if mime_type.startswith("image/"):
|
||||||
BrowseMediaSource(
|
media_class = MediaClass.IMAGE
|
||||||
domain=DOMAIN,
|
can_play = False
|
||||||
identifier=(
|
thumb_mime_type = mime_type
|
||||||
f"{identifier.unique_id}/"
|
else:
|
||||||
f"{identifier.album_id}/"
|
media_class = MediaClass.VIDEO
|
||||||
f"{asset.asset_id}/"
|
can_play = True
|
||||||
f"{asset.file_name}"
|
thumb_mime_type = "image/jpeg"
|
||||||
),
|
|
||||||
media_class=MediaClass.VIDEO,
|
ret.append(
|
||||||
media_content_type=asset.mime_type,
|
BrowseMediaSource(
|
||||||
title=asset.file_name,
|
domain=DOMAIN,
|
||||||
can_play=True,
|
identifier=(
|
||||||
can_expand=False,
|
f"{identifier.unique_id}|albums|"
|
||||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
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
|
return ret
|
||||||
|
|
||||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||||
"""Resolve media to a url."""
|
"""Resolve media to a url."""
|
||||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
try:
|
||||||
if identifier.file_name is None:
|
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||||
raise Unresolvable("No file name")
|
except IndexError as err:
|
||||||
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
raise Unresolvable(
|
||||||
if not isinstance(mime_type, str):
|
f"Could not parse identifier: {item.identifier}"
|
||||||
raise Unresolvable("No file extension")
|
) from err
|
||||||
|
|
||||||
|
if identifier.mime_type is None:
|
||||||
|
raise Unresolvable(
|
||||||
|
f"Could not resolve identifier that has no mime-type: {item.identifier}"
|
||||||
|
)
|
||||||
|
|
||||||
return PlayMedia(
|
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):
|
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||||
raise HTTPNotFound
|
raise HTTPNotFound
|
||||||
|
|
||||||
asset_id, file_name, size = location.split("/")
|
try:
|
||||||
mime_type, _ = mimetypes.guess_type(file_name)
|
asset_id, size, mime_type_base, mime_type_format = location.split("/")
|
||||||
if not isinstance(mime_type, str):
|
except ValueError as err:
|
||||||
raise HTTPNotFound
|
raise HTTPNotFound from err
|
||||||
|
|
||||||
entry: ImmichConfigEntry | None = (
|
entry: ImmichConfigEntry | None = (
|
||||||
self.hass.config_entries.async_entry_for_domain_unique_id(
|
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||||
|
@ -226,7 +243,7 @@ class ImmichMediaView(HomeAssistantView):
|
||||||
immich_api = entry.runtime_data.api
|
immich_api = entry.runtime_data.api
|
||||||
|
|
||||||
# stream response for videos
|
# stream response for videos
|
||||||
if mime_type.startswith("video/"):
|
if mime_type_base == "video":
|
||||||
try:
|
try:
|
||||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||||
except ImmichError as exc:
|
except ImmichError as exc:
|
||||||
|
@ -243,4 +260,4 @@ class ImmichMediaView(HomeAssistantView):
|
||||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||||
except ImmichError as exc:
|
except ImmichError as exc:
|
||||||
raise HTTPNotFound from 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.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.device import (
|
from homeassistant.helpers.device import (
|
||||||
|
async_entity_id_to_device_id,
|
||||||
async_remove_stale_devices_links_keep_entity_device,
|
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
|
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],
|
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,))
|
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["pyiskra"],
|
"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 homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
|
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
|
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
|
||||||
coordinator = JellyfinDataUpdateCoordinator(
|
coordinator = JellyfinDataUpdateCoordinator(
|
||||||
hass, entry, client, server_info, user_id
|
hass, entry, client, server_info, user_id
|
||||||
)
|
)
|
||||||
|
|
||||||
await coordinator.async_config_entry_first_refresh()
|
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.runtime_data = coordinator
|
||||||
entry.async_on_unload(client.stop)
|
entry.async_on_unload(client.stop)
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,10 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
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 homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DEFAULT_NAME, DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import JellyfinDataUpdateCoordinator
|
from .coordinator import JellyfinDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
|
||||||
"""Initialize the Jellyfin entity."""
|
"""Initialize the Jellyfin entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
|
||||||
identifiers={(DOMAIN, coordinator.server_id)},
|
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",
|
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
|
||||||
"iot_class": "calculated",
|
"iot_class": "calculated",
|
||||||
"loggers": ["hdate"],
|
"loggers": ["hdate"],
|
||||||
"requirements": ["hdate[astral]==1.1.0"],
|
"requirements": ["hdate[astral]==1.1.1"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ async def async_setup_entry(
|
||||||
JewishCalendarTimeSensor(config_entry, description)
|
JewishCalendarTimeSensor(config_entry, description)
|
||||||
for description in TIME_SENSORS
|
for description in TIME_SENSORS
|
||||||
)
|
)
|
||||||
async_add_entities(sensors)
|
async_add_entities(sensors, update_before_add=True)
|
||||||
|
|
||||||
|
|
||||||
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||||
|
@ -233,12 +233,7 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
|
||||||
|
|
||||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_update(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:
|
|
||||||
"""Update the state of the sensor."""
|
"""Update the state of the sensor."""
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
|
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
|
||||||
|
|
|
@ -11,8 +11,9 @@ from homeassistant.const import (
|
||||||
SERVICE_VOLUME_MUTE,
|
SERVICE_VOLUME_MUTE,
|
||||||
SERVICE_VOLUME_UP,
|
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 import config_validation as cv
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
DOMAIN = "keyboard"
|
DOMAIN = "keyboard"
|
||||||
|
@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Listen for keyboard events."""
|
"""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 = PyKeyboard()
|
||||||
keyboard.special_key_assignment()
|
keyboard.special_key_assignment()
|
||||||
|
|
|
@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(seconds=15)
|
SCAN_INTERVAL = timedelta(seconds=15)
|
||||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
|
@ -37,5 +37,5 @@
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["pylamarzocco"],
|
"loggers": ["pylamarzocco"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pylamarzocco==2.0.6"]
|
"requirements": ["pylamarzocco==2.0.8"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||||
key="prebrew_on",
|
key="prebrew_on",
|
||||||
translation_key="prebrew_time_on",
|
translation_key="prebrew_time_on",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
native_step=PRECISION_TENTHS,
|
native_step=PRECISION_TENTHS,
|
||||||
native_min_value=0,
|
native_min_value=0,
|
||||||
native_max_value=10,
|
native_max_value=10,
|
||||||
|
@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||||
key="prebrew_off",
|
key="prebrew_off",
|
||||||
translation_key="prebrew_time_off",
|
translation_key="prebrew_time_off",
|
||||||
device_class=NumberDeviceClass.DURATION,
|
device_class=NumberDeviceClass.DURATION,
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||||
native_step=PRECISION_TENTHS,
|
native_step=PRECISION_TENTHS,
|
||||||
native_min_value=0,
|
native_min_value=0,
|
||||||
native_max_value=10,
|
native_max_value=10,
|
||||||
|
|
|
@ -31,6 +31,9 @@ class LinkPlayConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle Zeroconf discovery."""
|
"""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)
|
session: ClientSession = await async_get_client_session(self.hass)
|
||||||
bridge: LinkPlayBridge | None = None
|
bridge: LinkPlayBridge | None = None
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,6 @@
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["linkplay"],
|
"loggers": ["linkplay"],
|
||||||
"requirements": ["python-linkplay==0.2.8"],
|
"requirements": ["python-linkplay==0.2.11"],
|
||||||
"zeroconf": ["_linkplay._tcp.local."]
|
"zeroconf": ["_linkplay._tcp.local."]
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,9 @@ import time
|
||||||
import lirc
|
import lirc
|
||||||
|
|
||||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
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 import config_validation as cv
|
||||||
|
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||||
|
|
||||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the LIRC capability."""
|
"""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)
|
# blocking=True gives unexpected behavior (multiple responses for 1 press)
|
||||||
# also by not blocking, we allow hass to shut down the thread gracefully
|
# also by not blocking, we allow hass to shut down the thread gracefully
|
||||||
# on exit.
|
# on exit.
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||||
"iot_class": "local_polling",
|
"iot_class": "local_polling",
|
||||||
"loggers": ["ical"],
|
"loggers": ["ical"],
|
||||||
"requirements": ["ical==9.2.5"]
|
"requirements": ["ical==10.0.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,5 @@
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||||
"iot_class": "local_polling",
|
"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,
|
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": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"pick_implementation": {
|
"pick_implementation": {
|
||||||
|
@ -9,7 +12,7 @@
|
||||||
"description": "The Lyric integration needs to re-authenticate your account."
|
"description": "The Lyric integration needs to re-authenticate your account."
|
||||||
},
|
},
|
||||||
"oauth_discovery": {
|
"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": {
|
"abort": {
|
||||||
|
|
|
@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
|
||||||
# don't discover this entry if the supported state list is empty
|
# don't discover this entry if the supported state list is empty
|
||||||
secondary_value_is_not=[],
|
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(
|
MatterDiscoverySchema(
|
||||||
platform=Platform.SENSOR,
|
platform=Platform.SENSOR,
|
||||||
entity_description=MatterSensorEntityDescription(
|
entity_description=MatterSensorEntityDescription(
|
||||||
key="TargetPositionLiftPercent100ths",
|
key="TargetPositionLiftPercent100ths",
|
||||||
entity_category=EntityCategory.DIAGNOSTIC,
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
entity_registry_enabled_default=False,
|
||||||
translation_key="window_covering_target_position",
|
translation_key="window_covering_target_position",
|
||||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
|
|
@ -390,12 +390,6 @@
|
||||||
"evse_user_max_charge_current": {
|
"evse_user_max_charge_current": {
|
||||||
"name": "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": {
|
"window_covering_target_position": {
|
||||||
"name": "Target opening 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