Compare commits
171 Commits
dev
...
2025.6.0b7
Author | SHA1 | Date |
---|---|---|
|
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 |
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
@ -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,
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
"""Diagnostics support for Amazon 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,
|
||||||
|
}
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
|
@ -3,31 +3,10 @@
|
||||||
"name": "Amazon Devices",
|
"name": "Amazon Devices",
|
||||||
"codeowners": ["@chemelli74"],
|
"codeowners": ["@chemelli74"],
|
||||||
"config_flow": true,
|
"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",
|
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||||
"integration_type": "hub",
|
"integration_type": "hub",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["aioamazondevices"],
|
"loggers": ["aioamazondevices"],
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["aioamazondevices==2.1.1"]
|
"requirements": ["aioamazondevices==3.0.6"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,7 +5,7 @@
|
||||||
"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}",
|
||||||
|
|
|
@ -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 = {}
|
||||||
|
|
|
@ -2,12 +2,18 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||||
from homeassistant.config_entries import ConfigEntry
|
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 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.helper_integration import async_handle_source_entity_changes
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
@ -17,6 +23,32 @@ 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)
|
||||||
|
|
||||||
|
entity_registry = er.async_get(hass)
|
||||||
|
entry.async_on_unload(
|
||||||
|
async_handle_source_entity_changes(
|
||||||
|
hass,
|
||||||
|
helper_config_entry_id=entry.entry_id,
|
||||||
|
get_helper_entity_id=lambda: entity_registry.async_get_entity_id(
|
||||||
|
SENSOR_DOMAIN, DOMAIN, 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.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -37,6 +37,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,9 +52,11 @@ 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,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers.system_info import async_get_system_info
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
from homeassistant.loader import bind_hass
|
from homeassistant.loader import bind_hass
|
||||||
from homeassistant.util.async_ import create_eager_task
|
from homeassistant.util.async_ import create_eager_task
|
||||||
|
@ -109,7 +112,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 +171,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."""
|
||||||
|
@ -546,6 +554,63 @@ 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
|
||||||
|
|
||||||
|
system_info = await async_get_system_info(hass)
|
||||||
|
|
||||||
|
def deprecated_setup_issue() -> None:
|
||||||
|
os_info = get_os_info(hass)
|
||||||
|
info = get_info(hass)
|
||||||
|
if os_info is None or info is None:
|
||||||
|
return
|
||||||
|
is_haos = info.get("hassos") is not None
|
||||||
|
arch = system_info["arch"]
|
||||||
|
board = os_info.get("board")
|
||||||
|
supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"}
|
||||||
|
if is_haos and arch == "armv7" and supported_board:
|
||||||
|
issue_id = "deprecated_os_"
|
||||||
|
if board in {"rpi3", "rpi4"}:
|
||||||
|
issue_id += "aarch64"
|
||||||
|
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||||
|
issue_id += "armv7"
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
"homeassistant",
|
||||||
|
issue_id,
|
||||||
|
breaks_in_ha_version="2025.12.0",
|
||||||
|
learn_more_url=DEPRECATION_URL,
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key=issue_id,
|
||||||
|
translation_placeholders={
|
||||||
|
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
deprecated_architecture = False
|
||||||
|
if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board):
|
||||||
|
deprecated_architecture = True
|
||||||
|
if not is_haos or deprecated_architecture:
|
||||||
|
issue_id = "deprecated"
|
||||||
|
if not is_haos:
|
||||||
|
issue_id += "_method"
|
||||||
|
if deprecated_architecture:
|
||||||
|
issue_id += "_architecture"
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
"homeassistant",
|
||||||
|
issue_id,
|
||||||
|
breaks_in_ha_version="2025.12.0",
|
||||||
|
learn_more_url=DEPRECATION_URL,
|
||||||
|
is_fixable=False,
|
||||||
|
severity=IssueSeverity.WARNING,
|
||||||
|
translation_key=issue_id,
|
||||||
|
translation_placeholders={
|
||||||
|
"installation_type": "OS" if is_haos else "Supervised",
|
||||||
|
"arch": arch,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
listener()
|
||||||
|
|
||||||
|
listener = coordinator.async_add_listener(deprecated_setup_issue)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
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)
|
||||||
|
|
|
@ -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,7 +4,7 @@ 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
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -38,7 +38,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,
|
||||||
|
@ -402,46 +401,10 @@ 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",
|
arch = info["arch"]
|
||||||
}
|
if arch == "armv7" and installation_type == "Container":
|
||||||
arch = info["arch"]
|
|
||||||
if arch == "armv7":
|
|
||||||
if installation_type == "OS":
|
|
||||||
# Local import to avoid circular dependencies
|
|
||||||
# We use the import helper because hassio
|
|
||||||
# may not be loaded yet and we don't want to
|
|
||||||
# do blocking I/O in the event loop to import it.
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
# pylint: disable-next=import-outside-toplevel
|
|
||||||
from homeassistant.components import hassio
|
|
||||||
else:
|
|
||||||
hassio = await async_import_module(
|
|
||||||
hass, "homeassistant.components.hassio"
|
|
||||||
)
|
|
||||||
os_info = hassio.get_os_info(hass)
|
|
||||||
assert os_info is not None
|
|
||||||
issue_id = "deprecated_os_"
|
|
||||||
board = os_info.get("board")
|
|
||||||
if board in {"rpi3", "rpi4"}:
|
|
||||||
issue_id += "aarch64"
|
|
||||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
|
||||||
issue_id += "armv7"
|
|
||||||
ir.async_create_issue(
|
|
||||||
hass,
|
|
||||||
DOMAIN,
|
|
||||||
issue_id,
|
|
||||||
breaks_in_ha_version="2025.12.0",
|
|
||||||
learn_more_url=DEPRECATION_URL,
|
|
||||||
is_fixable=False,
|
|
||||||
severity=IssueSeverity.WARNING,
|
|
||||||
translation_key=issue_id,
|
|
||||||
translation_placeholders={
|
|
||||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
elif installation_type == "Container":
|
|
||||||
ir.async_create_issue(
|
ir.async_create_issue(
|
||||||
hass,
|
hass,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
@ -452,29 +415,31 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||||
severity=IssueSeverity.WARNING,
|
severity=IssueSeverity.WARNING,
|
||||||
translation_key="deprecated_container_armv7",
|
translation_key="deprecated_container_armv7",
|
||||||
)
|
)
|
||||||
deprecated_architecture = False
|
deprecated_architecture = False
|
||||||
if arch in {"i386", "armhf"} or (arch == "armv7" and deprecated_method):
|
if arch in {"i386", "armhf"} or (
|
||||||
deprecated_architecture = True
|
arch == "armv7" and installation_type != "Container"
|
||||||
if deprecated_method or deprecated_architecture:
|
):
|
||||||
issue_id = "deprecated"
|
deprecated_architecture = True
|
||||||
if deprecated_method:
|
if deprecated_method or deprecated_architecture:
|
||||||
issue_id += "_method"
|
issue_id = "deprecated"
|
||||||
if deprecated_architecture:
|
if deprecated_method:
|
||||||
issue_id += "_architecture"
|
issue_id += "_method"
|
||||||
ir.async_create_issue(
|
if deprecated_architecture:
|
||||||
hass,
|
issue_id += "_architecture"
|
||||||
DOMAIN,
|
ir.async_create_issue(
|
||||||
issue_id,
|
hass,
|
||||||
breaks_in_ha_version="2025.12.0",
|
DOMAIN,
|
||||||
learn_more_url=DEPRECATION_URL,
|
issue_id,
|
||||||
is_fixable=False,
|
breaks_in_ha_version="2025.12.0",
|
||||||
severity=IssueSeverity.WARNING,
|
learn_more_url=DEPRECATION_URL,
|
||||||
translation_key=issue_id,
|
is_fixable=False,
|
||||||
translation_placeholders={
|
severity=IssueSeverity.WARNING,
|
||||||
"installation_type": installation_type,
|
translation_key=issue_id,
|
||||||
"arch": arch,
|
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",
|
||||||
|
|
|
@ -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}")
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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.10"],
|
||||||
"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"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -527,6 +527,7 @@ OVEN_PROGRAM_ID: dict[int, str] = {
|
||||||
116: "custom_program_20",
|
116: "custom_program_20",
|
||||||
323: "pyrolytic",
|
323: "pyrolytic",
|
||||||
326: "descale",
|
326: "descale",
|
||||||
|
327: "evaporate_water",
|
||||||
335: "shabbat_program",
|
335: "shabbat_program",
|
||||||
336: "yom_tov",
|
336: "yom_tov",
|
||||||
356: "defrost",
|
356: "defrost",
|
||||||
|
|
|
@ -542,6 +542,7 @@
|
||||||
"endive_strips": "Endive (strips)",
|
"endive_strips": "Endive (strips)",
|
||||||
"espresso": "Espresso",
|
"espresso": "Espresso",
|
||||||
"espresso_macchiato": "Espresso macchiato",
|
"espresso_macchiato": "Espresso macchiato",
|
||||||
|
"evaporate_water": "Evaporate water",
|
||||||
"express": "Express",
|
"express": "Express",
|
||||||
"express_20": "Express 20'",
|
"express_20": "Express 20'",
|
||||||
"extra_quiet": "Extra quiet",
|
"extra_quiet": "Extra quiet",
|
||||||
|
|
|
@ -39,6 +39,7 @@ from homeassistant.components.light import (
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
CONF_STATE_CLASS,
|
CONF_STATE_CLASS,
|
||||||
DEVICE_CLASS_UNITS,
|
DEVICE_CLASS_UNITS,
|
||||||
|
STATE_CLASS_UNITS,
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
|
@ -640,6 +641,13 @@ def validate_sensor_platform_config(
|
||||||
):
|
):
|
||||||
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
|
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom"
|
||||||
|
|
||||||
|
if (
|
||||||
|
(state_class := config.get(CONF_STATE_CLASS)) is not None
|
||||||
|
and state_class in STATE_CLASS_UNITS
|
||||||
|
and config.get(CONF_UNIT_OF_MEASUREMENT) not in STATE_CLASS_UNITS[state_class]
|
||||||
|
):
|
||||||
|
errors[CONF_UNIT_OF_MEASUREMENT] = "invalid_uom_for_state_class"
|
||||||
|
|
||||||
return errors
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
@ -676,11 +684,19 @@ class PlatformField:
|
||||||
@callback
|
@callback
|
||||||
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
|
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> Selector:
|
||||||
"""Return a context based unit of measurement selector."""
|
"""Return a context based unit of measurement selector."""
|
||||||
|
|
||||||
|
if (state_class := user_data.get(CONF_STATE_CLASS)) in STATE_CLASS_UNITS:
|
||||||
|
return SelectSelector(
|
||||||
|
SelectSelectorConfig(
|
||||||
|
options=[str(uom) for uom in STATE_CLASS_UNITS[state_class]],
|
||||||
|
sort=True,
|
||||||
|
custom_value=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user_data is None
|
device_class := user_data.get(CONF_DEVICE_CLASS)
|
||||||
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
|
) is None or device_class not in DEVICE_CLASS_UNITS:
|
||||||
or device_class not in DEVICE_CLASS_UNITS
|
|
||||||
):
|
|
||||||
return TEXT_SELECTOR
|
return TEXT_SELECTOR
|
||||||
return SelectSelector(
|
return SelectSelector(
|
||||||
SelectSelectorConfig(
|
SelectSelectorConfig(
|
||||||
|
|
|
@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
||||||
DEVICE_CLASS_UNITS,
|
DEVICE_CLASS_UNITS,
|
||||||
DEVICE_CLASSES_SCHEMA,
|
DEVICE_CLASSES_SCHEMA,
|
||||||
ENTITY_ID_FORMAT,
|
ENTITY_ID_FORMAT,
|
||||||
|
STATE_CLASS_UNITS,
|
||||||
STATE_CLASSES_SCHEMA,
|
STATE_CLASSES_SCHEMA,
|
||||||
RestoreSensor,
|
RestoreSensor,
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
|
@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||||
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
|
f"got `{CONF_DEVICE_CLASS}` '{device_class}'"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
(state_class := config.get(CONF_STATE_CLASS)) is not None
|
||||||
|
and state_class in STATE_CLASS_UNITS
|
||||||
|
and (unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT))
|
||||||
|
not in STATE_CLASS_UNITS[state_class]
|
||||||
|
):
|
||||||
|
raise vol.Invalid(
|
||||||
|
f"The unit of measurement '{unit_of_measurement}' is not valid "
|
||||||
|
f"together with state class '{state_class}'"
|
||||||
|
)
|
||||||
|
|
||||||
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
if (device_class := config.get(CONF_DEVICE_CLASS)) is None or (
|
||||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||||
) is None:
|
) is None:
|
||||||
|
|
|
@ -644,6 +644,7 @@
|
||||||
"invalid_template": "Invalid template",
|
"invalid_template": "Invalid template",
|
||||||
"invalid_supported_color_modes": "Invalid supported color modes selection",
|
"invalid_supported_color_modes": "Invalid supported color modes selection",
|
||||||
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
"invalid_uom": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected device class, please either remove the device class, select a device class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||||
|
"invalid_uom_for_state_class": "The unit of measurement \"{unit_of_measurement}\" is not supported by the selected state class, please either remove the state class, select a state class which supports \"{unit_of_measurement}\", or pick a supported unit of measurement from the list",
|
||||||
"invalid_url": "Invalid URL",
|
"invalid_url": "Invalid URL",
|
||||||
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
|
"last_reset_not_with_state_class_total": "The last reset value template option should be used with state class 'Total' only",
|
||||||
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",
|
"max_below_min_kelvin": "Max Kelvin value should be greater than min Kelvin value",
|
||||||
|
|
|
@ -293,8 +293,8 @@ class MyUplinkDevicePointSensor(MyUplinkEntity, SensorEntity):
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Sensor state value."""
|
"""Sensor state value."""
|
||||||
device_point = self.coordinator.data.points[self.device_id][self.point_id]
|
device_point = self.coordinator.data.points[self.device_id].get(self.point_id)
|
||||||
if device_point.value == MARKER_FOR_UNKNOWN_VALUE:
|
if device_point is None or device_point.value == MARKER_FOR_UNKNOWN_VALUE:
|
||||||
return None
|
return None
|
||||||
return device_point.value # type: ignore[no-any-return]
|
return device_point.value # type: ignore[no-any-return]
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,6 @@ from homeassistant.helpers.selector import (
|
||||||
SelectSelectorConfig,
|
SelectSelectorConfig,
|
||||||
SelectSelectorMode,
|
SelectSelectorMode,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
|
||||||
from homeassistant.util import get_random_string
|
from homeassistant.util import get_random_string
|
||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
|
@ -441,9 +440,3 @@ class NestFlowHandler(
|
||||||
if self._structure_config_title:
|
if self._structure_config_title:
|
||||||
title = self._structure_config_title
|
title = self._structure_config_title
|
||||||
return self.async_create_entry(title=title, data=self._data)
|
return self.async_create_entry(title=title, data=self._data)
|
||||||
|
|
||||||
async def async_step_dhcp(
|
|
||||||
self, discovery_info: DhcpServiceInfo
|
|
||||||
) -> ConfigFlowResult:
|
|
||||||
"""Handle a flow initialized by discovery."""
|
|
||||||
return await self.async_step_user()
|
|
||||||
|
|
|
@ -47,6 +47,9 @@
|
||||||
"reauth_confirm": {
|
"reauth_confirm": {
|
||||||
"title": "[%key:common::config_flow::title::reauth%]",
|
"title": "[%key:common::config_flow::title::reauth%]",
|
||||||
"description": "The Nest integration needs to re-authenticate your account"
|
"description": "The Nest integration needs to re-authenticate your account"
|
||||||
|
},
|
||||||
|
"oauth_discovery": {
|
||||||
|
"description": "Home Assistant has found a Google Nest device on your network. Be aware that the set up of Google Nest is more complicated than most other integrations and requires setting up a Nest Device Access project which **requires paying Google a US $5 fee**. Press **Submit** to continue setting up Google Nest."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""NextBus data update coordinator."""
|
"""NextBus data update coordinator."""
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any, override
|
||||||
|
|
||||||
from py_nextbus import NextBusClient
|
from py_nextbus import NextBusClient
|
||||||
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
|
from py_nextbus.client import NextBusFormatError, NextBusHTTPError
|
||||||
|
@ -15,8 +15,14 @@ from .util import RouteStop
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# At what percentage of the request limit should the coordinator pause making requests
|
||||||
|
UPDATE_INTERVAL_SECONDS = 30
|
||||||
|
THROTTLE_PRECENTAGE = 80
|
||||||
|
|
||||||
class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
|
||||||
|
class NextBusDataUpdateCoordinator(
|
||||||
|
DataUpdateCoordinator[dict[RouteStop, dict[str, Any]]]
|
||||||
|
):
|
||||||
"""Class to manage fetching NextBus data."""
|
"""Class to manage fetching NextBus data."""
|
||||||
|
|
||||||
def __init__(self, hass: HomeAssistant, agency: str) -> None:
|
def __init__(self, hass: HomeAssistant, agency: str) -> None:
|
||||||
|
@ -26,7 +32,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
_LOGGER,
|
_LOGGER,
|
||||||
config_entry=None, # It is shared between multiple entries
|
config_entry=None, # It is shared between multiple entries
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=30),
|
update_interval=timedelta(seconds=UPDATE_INTERVAL_SECONDS),
|
||||||
)
|
)
|
||||||
self.client = NextBusClient(agency_id=agency)
|
self.client = NextBusClient(agency_id=agency)
|
||||||
self._agency = agency
|
self._agency = agency
|
||||||
|
@ -49,9 +55,26 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Check if this coordinator is tracking any routes."""
|
"""Check if this coordinator is tracking any routes."""
|
||||||
return len(self._route_stops) > 0
|
return len(self._route_stops) > 0
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, Any]:
|
@override
|
||||||
|
async def _async_update_data(self) -> dict[RouteStop, dict[str, Any]]:
|
||||||
"""Fetch data from NextBus."""
|
"""Fetch data from NextBus."""
|
||||||
|
|
||||||
|
if (
|
||||||
|
# If we have predictions, check the rate limit
|
||||||
|
self._predictions
|
||||||
|
# If are over our rate limit percentage, we should throttle
|
||||||
|
and self.client.rate_limit_percent >= THROTTLE_PRECENTAGE
|
||||||
|
# But only if we have a reset time to unthrottle
|
||||||
|
and self.client.rate_limit_reset is not None
|
||||||
|
# Unless we are after the reset time
|
||||||
|
and datetime.now() < self.client.rate_limit_reset
|
||||||
|
):
|
||||||
|
self.logger.debug(
|
||||||
|
"Rate limit threshold reached. Skipping updates for. Routes: %s",
|
||||||
|
str(self._route_stops),
|
||||||
|
)
|
||||||
|
return self._predictions
|
||||||
|
|
||||||
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
|
_stops_to_route_stops: dict[str, set[RouteStop]] = {}
|
||||||
for route_stop in self._route_stops:
|
for route_stop in self._route_stops:
|
||||||
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
|
_stops_to_route_stops.setdefault(route_stop.stop_id, set()).add(route_stop)
|
||||||
|
@ -60,7 +83,7 @@ class NextBusDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
|
"Updating data from API. Routes: %s", str(_stops_to_route_stops)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _update_data() -> dict:
|
def _update_data() -> dict[RouteStop, dict[str, Any]]:
|
||||||
"""Fetch data from NextBus."""
|
"""Fetch data from NextBus."""
|
||||||
self.logger.debug("Updating data from API (executor)")
|
self.logger.debug("Updating data from API (executor)")
|
||||||
predictions: dict[RouteStop, dict[str, Any]] = {}
|
predictions: dict[RouteStop, dict[str, Any]] = {}
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["py_nextbus"],
|
"loggers": ["py_nextbus"],
|
||||||
"requirements": ["py-nextbusnext==2.1.2"]
|
"requirements": ["py-nextbusnext==2.2.0"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ SUPPORT_FLAGS = (
|
||||||
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
|
PRESET_MODES = [PRESET_NONE, PRESET_COMFORT, PRESET_ECO, PRESET_AWAY]
|
||||||
|
|
||||||
MIN_TEMPERATURE = 7
|
MIN_TEMPERATURE = 7
|
||||||
MAX_TEMPERATURE = 40
|
MAX_TEMPERATURE = 30
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
|
|
|
@ -8,6 +8,6 @@
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["pynordpool"],
|
"loggers": ["pynordpool"],
|
||||||
"quality_scale": "platinum",
|
"quality_scale": "platinum",
|
||||||
"requirements": ["pynordpool==0.2.4"],
|
"requirements": ["pynordpool==0.3.0"],
|
||||||
"single_config_entry": true
|
"single_config_entry": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,10 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
|
||||||
@callback
|
@callback
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Update the entity from the latest data."""
|
"""Update the entity from the latest data."""
|
||||||
|
self._update_attrs()
|
||||||
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
def _update_attrs(self) -> None:
|
||||||
data = self.coordinator.data
|
data = self.coordinator.data
|
||||||
|
|
||||||
for key in ("from_time", "to_time", "from_uv", "to_uv"):
|
for key in ("from_time", "to_time", "from_uv", "to_uv"):
|
||||||
|
@ -78,5 +82,3 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorEntity):
|
||||||
ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt),
|
ATTR_PROTECTION_WINDOW_STARTING_TIME: as_local(from_dt),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
super()._handle_coordinator_update()
|
|
||||||
|
|
|
@ -31,3 +31,8 @@ class OpenUvEntity(CoordinatorEntity):
|
||||||
name="OpenUV",
|
name="OpenUV",
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._update_attrs()
|
||||||
|
|
||||||
|
def _update_attrs(self) -> None:
|
||||||
|
"""Override point for updating attributes during init."""
|
||||||
|
|
|
@ -10,7 +10,6 @@ from homeassistant.components.sensor import (
|
||||||
)
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
CONCENTRATION_PARTS_PER_MILLION,
|
|
||||||
DEGREE,
|
DEGREE,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
UV_INDEX,
|
UV_INDEX,
|
||||||
|
@ -170,7 +169,7 @@ AIRPOLLUTION_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||||
),
|
),
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key=ATTR_API_AIRPOLLUTION_CO,
|
key=ATTR_API_AIRPOLLUTION_CO,
|
||||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||||
device_class=SensorDeviceClass.CO,
|
device_class=SensorDeviceClass.CO,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["opower"],
|
"loggers": ["opower"],
|
||||||
"requirements": ["opower==0.12.2"]
|
"requirements": ["opower==0.12.3"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
"""The pandora component."""
|
"""The pandora component."""
|
||||||
|
|
||||||
|
DOMAIN = "pandora"
|
||||||
|
|
|
@ -27,10 +27,13 @@ from homeassistant.const import (
|
||||||
SERVICE_VOLUME_DOWN,
|
SERVICE_VOLUME_DOWN,
|
||||||
SERVICE_VOLUME_UP,
|
SERVICE_VOLUME_UP,
|
||||||
)
|
)
|
||||||
from homeassistant.core import Event, HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||||
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__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -53,6 +56,21 @@ def setup_platform(
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Pandora media player platform."""
|
"""Set up the Pandora media player 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": "Pandora",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
if not _pianobar_exists():
|
if not _pianobar_exists():
|
||||||
return
|
return
|
||||||
pandora = PandoraMediaPlayer("Pandora")
|
pandora = PandoraMediaPlayer("Pandora")
|
||||||
|
|
|
@ -6,5 +6,5 @@
|
||||||
"documentation": "https://www.home-assistant.io/integrations/picnic",
|
"documentation": "https://www.home-assistant.io/integrations/picnic",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"loggers": ["python_picnic_api2"],
|
"loggers": ["python_picnic_api2"],
|
||||||
"requirements": ["python-picnic-api2==1.2.4"]
|
"requirements": ["python-picnic-api2==1.3.1"]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,4 @@ from typing import Final
|
||||||
|
|
||||||
DOMAIN: Final = "powerfox"
|
DOMAIN: Final = "powerfox"
|
||||||
LOGGER = logging.getLogger(__package__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
SCAN_INTERVAL = timedelta(minutes=1)
|
SCAN_INTERVAL = timedelta(seconds=10)
|
||||||
|
|
|
@ -15,5 +15,5 @@
|
||||||
"integration_type": "device",
|
"integration_type": "device",
|
||||||
"iot_class": "local_push",
|
"iot_class": "local_push",
|
||||||
"quality_scale": "bronze",
|
"quality_scale": "bronze",
|
||||||
"requirements": ["pyprobeplus==1.0.0"]
|
"requirements": ["pyprobeplus==1.0.1"]
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue