mirror of
https://github.com/home-assistant/core.git
synced 2026-01-10 17:47:16 +01:00
Compare commits
102 Commits
copilot-in
...
2025.6.0b5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
6
homeassistant/brands/shelly.json
Normal file
6
homeassistant/brands/shelly.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "shelly",
|
||||
"name": "shelly",
|
||||
"integrations": ["shelly"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
@@ -40,9 +40,10 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(TimeoutError):
|
||||
async with timeout(5):
|
||||
hubs: list[aiopulse.Hub] = [
|
||||
hubs = [
|
||||
hub
|
||||
async for hub in aiopulse.Hub.discover()
|
||||
if hub.id not in already_configured
|
||||
|
||||
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.positive_int,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
66
homeassistant/components/amazon_devices/diagnostics.py
Normal file
66
homeassistant/components/amazon_devices/diagnostics.py
Normal file
@@ -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,
|
||||
}
|
||||
@@ -50,4 +50,8 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return super().available and self._serial_num in self.coordinator.data
|
||||
return (
|
||||
super().available
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
|
||||
@@ -4,30 +4,119 @@
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{ "macaddress": "007147*" },
|
||||
{ "macaddress": "00FC8B*" },
|
||||
{ "macaddress": "0812A5*" },
|
||||
{ "macaddress": "086AE5*" },
|
||||
{ "macaddress": "08849D*" },
|
||||
{ "macaddress": "089115*" },
|
||||
{ "macaddress": "08A6BC*" },
|
||||
{ "macaddress": "08C224*" },
|
||||
{ "macaddress": "0CDC91*" },
|
||||
{ "macaddress": "0CEE99*" },
|
||||
{ "macaddress": "1009F9*" },
|
||||
{ "macaddress": "109693*" },
|
||||
{ "macaddress": "10BF67*" },
|
||||
{ "macaddress": "10CE02*" },
|
||||
{ "macaddress": "140AC5*" },
|
||||
{ "macaddress": "149138*" },
|
||||
{ "macaddress": "1848BE*" },
|
||||
{ "macaddress": "1C12B0*" },
|
||||
{ "macaddress": "1C4D66*" },
|
||||
{ "macaddress": "1C93C4*" },
|
||||
{ "macaddress": "1CFE2B*" },
|
||||
{ "macaddress": "244CE3*" },
|
||||
{ "macaddress": "24CE33*" },
|
||||
{ "macaddress": "2873F6*" },
|
||||
{ "macaddress": "2C71FF*" },
|
||||
{ "macaddress": "34AFB3*" },
|
||||
{ "macaddress": "34D270*" },
|
||||
{ "macaddress": "38F73D*" },
|
||||
{ "macaddress": "3C5CC4*" },
|
||||
{ "macaddress": "3CE441*" },
|
||||
{ "macaddress": "440049*" },
|
||||
{ "macaddress": "40A2DB*" },
|
||||
{ "macaddress": "40A9CF*" },
|
||||
{ "macaddress": "40B4CD*" },
|
||||
{ "macaddress": "443D54*" },
|
||||
{ "macaddress": "44650D*" },
|
||||
{ "macaddress": "485F2D*" },
|
||||
{ "macaddress": "48785E*" },
|
||||
{ "macaddress": "48B423*" },
|
||||
{ "macaddress": "4C1744*" },
|
||||
{ "macaddress": "4CEFC0*" },
|
||||
{ "macaddress": "5007C3*" },
|
||||
{ "macaddress": "50D45C*" },
|
||||
{ "macaddress": "50DCE7*" },
|
||||
{ "macaddress": "50F5DA*" },
|
||||
{ "macaddress": "5C415A*" },
|
||||
{ "macaddress": "6837E9*" },
|
||||
{ "macaddress": "6854FD*" },
|
||||
{ "macaddress": "689A87*" },
|
||||
{ "macaddress": "68B691*" },
|
||||
{ "macaddress": "68DBF5*" },
|
||||
{ "macaddress": "68F63B*" },
|
||||
{ "macaddress": "6C0C9A*" },
|
||||
{ "macaddress": "6C5697*" },
|
||||
{ "macaddress": "7458F3*" },
|
||||
{ "macaddress": "74C246*" },
|
||||
{ "macaddress": "74D637*" },
|
||||
{ "macaddress": "74E20C*" },
|
||||
{ "macaddress": "74ECB2*" },
|
||||
{ "macaddress": "786C84*" },
|
||||
{ "macaddress": "78A03F*" },
|
||||
{ "macaddress": "7C6166*" },
|
||||
{ "macaddress": "7C6305*" },
|
||||
{ "macaddress": "7CD566*" },
|
||||
{ "macaddress": "8871E5*" },
|
||||
{ "macaddress": "901195*" },
|
||||
{ "macaddress": "90235B*" },
|
||||
{ "macaddress": "90A822*" },
|
||||
{ "macaddress": "90F82E*" },
|
||||
{ "macaddress": "943A91*" },
|
||||
{ "macaddress": "98226E*" },
|
||||
{ "macaddress": "98CCF3*" },
|
||||
{ "macaddress": "9CC8E9*" },
|
||||
{ "macaddress": "A002DC*" },
|
||||
{ "macaddress": "A0D2B1*" },
|
||||
{ "macaddress": "A40801*" },
|
||||
{ "macaddress": "A8E621*" },
|
||||
{ "macaddress": "AC416A*" },
|
||||
{ "macaddress": "AC63BE*" },
|
||||
{ "macaddress": "ACCCFC*" },
|
||||
{ "macaddress": "B0739C*" },
|
||||
{ "macaddress": "B0CFCB*" },
|
||||
{ "macaddress": "B0F7C4*" },
|
||||
{ "macaddress": "B85F98*" },
|
||||
{ "macaddress": "C091B9*" },
|
||||
{ "macaddress": "C095CF*" },
|
||||
{ "macaddress": "C49500*" },
|
||||
{ "macaddress": "C86C3D*" },
|
||||
{ "macaddress": "CC9EA2*" },
|
||||
{ "macaddress": "CCF735*" },
|
||||
{ "macaddress": "DC54D7*" },
|
||||
{ "macaddress": "D8BE65*" },
|
||||
{ "macaddress": "EC2BEB*" }
|
||||
{ "macaddress": "D8FBD6*" },
|
||||
{ "macaddress": "DC91BF*" },
|
||||
{ "macaddress": "DCA0D0*" },
|
||||
{ "macaddress": "E0F728*" },
|
||||
{ "macaddress": "EC2BEB*" },
|
||||
{ "macaddress": "EC8AC4*" },
|
||||
{ "macaddress": "ECA138*" },
|
||||
{ "macaddress": "F02F9E*" },
|
||||
{ "macaddress": "F0272D*" },
|
||||
{ "macaddress": "F0F0A4*" },
|
||||
{ "macaddress": "F4032A*" },
|
||||
{ "macaddress": "F854B8*" },
|
||||
{ "macaddress": "FC492D*" },
|
||||
{ "macaddress": "FC65DE*" },
|
||||
{ "macaddress": "FCA183*" },
|
||||
{ "macaddress": "FCE9D8*" }
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==2.1.1"]
|
||||
"requirements": ["aioamazondevices==3.0.5"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password sent to your email address."
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.48.2"
|
||||
"habluetooth==3.49.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.28"]
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
||||
@@ -21,7 +21,11 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -90,6 +94,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The dlib_face_detect component."""
|
||||
|
||||
DOMAIN = "dlib_face_detect"
|
||||
|
||||
@@ -11,10 +11,17 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@@ -25,6 +32,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Detect",
|
||||
},
|
||||
)
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
add_entities(
|
||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""The dlib_face_identify component."""
|
||||
|
||||
CONF_FACES = "faces"
|
||||
DOMAIN = "dlib_face_identify"
|
||||
|
||||
@@ -15,14 +15,20 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_FACES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FACES = "faces"
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -39,6 +45,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Identify",
|
||||
},
|
||||
)
|
||||
|
||||
confidence: float = config[CONF_CONFIDENCE]
|
||||
faces: dict[str, str] = config[CONF_FACES]
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Any
|
||||
from typing import Any, Literal
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||
"""Validate hostname."""
|
||||
|
||||
async def async_check(
|
||||
hostname: str, resolver: str, qtype: str, port: int = 53
|
||||
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
|
||||
) -> bool:
|
||||
"""Return if able to resolve hostname."""
|
||||
result = False
|
||||
result: bool = False
|
||||
with contextlib.suppress(DNSError):
|
||||
result = bool(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
result: dict[str, bool] = {}
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,17 +23,18 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -58,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
}
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==31.1.0",
|
||||
"aioesphomeapi==32.0.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.15.1"
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ async def async_setup_entry(
|
||||
name=f"Freebox {sensor_name}",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
for sensor_name in router.sensors_temperature
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250526.0"]
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==9.2.5"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,12 @@ from .const import (
|
||||
UNITS_IMPERIAL,
|
||||
UNITS_METRIC,
|
||||
)
|
||||
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
|
||||
from .helpers import (
|
||||
InvalidApiKeyException,
|
||||
PermissionDeniedException,
|
||||
UnknownException,
|
||||
validate_config_entry,
|
||||
)
|
||||
|
||||
RECONFIGURE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -188,6 +193,8 @@ async def validate_input(
|
||||
user_input[CONF_ORIGIN],
|
||||
user_input[CONF_DESTINATION],
|
||||
)
|
||||
except PermissionDeniedException:
|
||||
return {"base": "permission_denied"}
|
||||
except InvalidApiKeyException:
|
||||
return {"base": "invalid_auth"}
|
||||
except TimeoutError:
|
||||
|
||||
@@ -7,6 +7,7 @@ from google.api_core.exceptions import (
|
||||
Forbidden,
|
||||
GatewayTimeout,
|
||||
GoogleAPIError,
|
||||
PermissionDenied,
|
||||
Unauthorized,
|
||||
)
|
||||
from google.maps.routing_v2 import (
|
||||
@@ -19,10 +20,18 @@ from google.maps.routing_v2 import (
|
||||
from google.type import latlng_pb2
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,7 +46,7 @@ def convert_to_waypoint(hass: HomeAssistant, location: str) -> Waypoint | None:
|
||||
try:
|
||||
formatted_coordinates = coordinates.split(",")
|
||||
vol.Schema(cv.gps(formatted_coordinates))
|
||||
except (AttributeError, vol.ExactSequenceInvalid):
|
||||
except (AttributeError, vol.Invalid):
|
||||
return Waypoint(address=location)
|
||||
return Waypoint(
|
||||
location=Location(
|
||||
@@ -67,6 +76,9 @@ async def validate_config_entry(
|
||||
await client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", field_mask)]
|
||||
)
|
||||
except PermissionDenied as permission_error:
|
||||
_LOGGER.error("Permission denied: %s", permission_error.message)
|
||||
raise PermissionDeniedException from permission_error
|
||||
except (Unauthorized, Forbidden) as unauthorized_error:
|
||||
_LOGGER.error("Request denied: %s", unauthorized_error.message)
|
||||
raise InvalidApiKeyException from unauthorized_error
|
||||
@@ -84,3 +96,30 @@ class InvalidApiKeyException(Exception):
|
||||
|
||||
class UnknownException(Exception):
|
||||
"""Unknown API Error."""
|
||||
|
||||
|
||||
class PermissionDeniedException(Exception):
|
||||
"""Permission Denied Error."""
|
||||
|
||||
|
||||
def create_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Create an issue for the Routes API being disabled."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
f"routes_api_disabled_{entry.entry_id}",
|
||||
learn_more_url="https://www.home-assistant.io/integrations/google_travel_time#setup",
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.ERROR,
|
||||
translation_key="routes_api_disabled",
|
||||
translation_placeholders={
|
||||
"entry_title": entry.title,
|
||||
"enable_api_url": "https://cloud.google.com/endpoints/docs/openapi/enable-api",
|
||||
"api_key_restrictions_url": "https://cloud.google.com/docs/authentication/api-keys#adding-api-restrictions",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def delete_routes_api_disabled_issue(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Delete the issue for the Routes API being disabled."""
|
||||
async_delete_issue(hass, DOMAIN, f"routes_api_disabled_{entry.entry_id}")
|
||||
|
||||
@@ -7,7 +7,7 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from google.api_core.client_options import ClientOptions
|
||||
from google.api_core.exceptions import GoogleAPIError
|
||||
from google.api_core.exceptions import GoogleAPIError, PermissionDenied
|
||||
from google.maps.routing_v2 import (
|
||||
ComputeRoutesRequest,
|
||||
Route,
|
||||
@@ -58,7 +58,11 @@ from .const import (
|
||||
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
|
||||
UNITS_TO_GOOGLE_SDK_ENUM,
|
||||
)
|
||||
from .helpers import convert_to_waypoint
|
||||
from .helpers import (
|
||||
convert_to_waypoint,
|
||||
create_routes_api_disabled_issue,
|
||||
delete_routes_api_disabled_issue,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -271,8 +275,14 @@ class GoogleTravelTimeSensor(SensorEntity):
|
||||
response = await self._client.compute_routes(
|
||||
request, metadata=[("x-goog-fieldmask", FIELD_MASK)]
|
||||
)
|
||||
_LOGGER.debug("Received response: %s", response)
|
||||
if response is not None and len(response.routes) > 0:
|
||||
self._route = response.routes[0]
|
||||
delete_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
except PermissionDenied:
|
||||
_LOGGER.error("Routes API is disabled for this API key")
|
||||
create_routes_api_disabled_issue(self.hass, self._config_entry)
|
||||
self._route = None
|
||||
except GoogleAPIError as ex:
|
||||
_LOGGER.error("Error getting travel time: %s", ex)
|
||||
self._route = None
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"permission_denied": "The Routes API is not enabled for this API key. Please see the setup instructions for detailed information.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]"
|
||||
@@ -100,5 +101,11 @@
|
||||
"fewer_transfers": "Fewer transfers"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"routes_api_disabled": {
|
||||
"title": "The Routes API must be enabled",
|
||||
"description": "Your Google Travel Time integration `{entry_title}` uses an API key which does not have the Routes API enabled.\n\n Please follow the instructions to [enable the API for your project]({enable_api_url}) and make sure your [API key restrictions]({api_key_restrictions_url}) allow access to the Routes API.\n\n After enabling the API this issue will be resolved automatically."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The gstreamer component."""
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
@@ -19,16 +19,18 @@ from homeassistant.components.media_player import (
|
||||
async_process_play_media_url,
|
||||
)
|
||||
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_PIPELINE = "pipeline"
|
||||
|
||||
DOMAIN = "gstreamer"
|
||||
|
||||
PLATFORM_SCHEMA = MEDIA_PLAYER_PLATFORM_SCHEMA.extend(
|
||||
{vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PIPELINE): cv.string}
|
||||
@@ -48,6 +50,20 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Gstreamer platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "GStreamer",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
pipeline = config.get(CONF_PIPELINE)
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The hddtemp component."""
|
||||
|
||||
DOMAIN = "hddtemp"
|
||||
|
||||
@@ -22,11 +22,14 @@ from homeassistant.const import (
|
||||
CONF_PORT,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_DEVICE = "device"
|
||||
@@ -56,6 +59,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the HDDTemp sensor."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "hddtemp",
|
||||
},
|
||||
)
|
||||
|
||||
name = config.get(CONF_NAME)
|
||||
host = config.get(CONF_HOST)
|
||||
port = config.get(CONF_PORT)
|
||||
|
||||
@@ -10,17 +10,17 @@
|
||||
"macaddress": "C8D778*"
|
||||
},
|
||||
{
|
||||
"hostname": "(bosch|siemens)-*",
|
||||
"hostname": "(balay|bosch|neff|siemens)-*",
|
||||
"macaddress": "68A40E*"
|
||||
},
|
||||
{
|
||||
"hostname": "siemens-*",
|
||||
"hostname": "(siemens|neff)-*",
|
||||
"macaddress": "38B4D3*"
|
||||
}
|
||||
],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.0"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"title": "The {integration_title} YAML configuration is being removed",
|
||||
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_config_flow_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove all \"{integration_title}\" config entries to fix this issue."
|
||||
},
|
||||
"deprecated_system_packages_yaml_integration": {
|
||||
"title": "The {integration_title} integration is being removed",
|
||||
"description": "The {integration_title} integration is being removed as it requires additional system packages, which can't be installed on supported Home Assistant installations. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioimmich"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioimmich==0.6.0"]
|
||||
"requirements": ["aioimmich==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import getLogger
|
||||
import mimetypes
|
||||
|
||||
from aiohttp.web import HTTPNotFound, Request, Response, StreamResponse
|
||||
from aioimmich.exceptions import ImmichError
|
||||
@@ -30,11 +29,8 @@ LOGGER = getLogger(__name__)
|
||||
|
||||
async def async_get_media_source(hass: HomeAssistant) -> MediaSource:
|
||||
"""Set up Immich media source."""
|
||||
entries = hass.config_entries.async_entries(
|
||||
DOMAIN, include_disabled=False, include_ignore=False
|
||||
)
|
||||
hass.http.register_view(ImmichMediaView(hass))
|
||||
return ImmichMediaSource(hass, entries)
|
||||
return ImmichMediaSource(hass)
|
||||
|
||||
|
||||
class ImmichMediaSourceIdentifier:
|
||||
@@ -42,12 +38,14 @@ class ImmichMediaSourceIdentifier:
|
||||
|
||||
def __init__(self, identifier: str) -> None:
|
||||
"""Split identifier into parts."""
|
||||
parts = identifier.split("/")
|
||||
# coonfig_entry.unique_id/album_id/asset_it/filename
|
||||
parts = identifier.split("|")
|
||||
# config_entry.unique_id|collection|collection_id|asset_id|file_name|mime_type
|
||||
self.unique_id = parts[0]
|
||||
self.album_id = parts[1] if len(parts) > 1 else None
|
||||
self.asset_id = parts[2] if len(parts) > 2 else None
|
||||
self.file_name = parts[3] if len(parts) > 2 else None
|
||||
self.collection = parts[1] if len(parts) > 1 else None
|
||||
self.collection_id = parts[2] if len(parts) > 2 else None
|
||||
self.asset_id = parts[3] if len(parts) > 3 else None
|
||||
self.file_name = parts[4] if len(parts) > 3 else None
|
||||
self.mime_type = parts[5] if len(parts) > 3 else None
|
||||
|
||||
|
||||
class ImmichMediaSource(MediaSource):
|
||||
@@ -55,18 +53,17 @@ class ImmichMediaSource(MediaSource):
|
||||
|
||||
name = "Immich"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entries: list[ConfigEntry]) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize Immich media source."""
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
self.entries = entries
|
||||
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
) -> BrowseMediaSource:
|
||||
"""Return media."""
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
if not (entries := self.hass.config_entries.async_loaded_entries(DOMAIN)):
|
||||
raise BrowseError("Immich is not configured")
|
||||
return BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -78,15 +75,16 @@ class ImmichMediaSource(MediaSource):
|
||||
can_expand=True,
|
||||
children_media_class=MediaClass.DIRECTORY,
|
||||
children=[
|
||||
*await self._async_build_immich(item),
|
||||
*await self._async_build_immich(item, entries),
|
||||
],
|
||||
)
|
||||
|
||||
async def _async_build_immich(
|
||||
self, item: MediaSourceItem
|
||||
self, item: MediaSourceItem, entries: list[ConfigEntry]
|
||||
) -> list[BrowseMediaSource]:
|
||||
"""Handle browsing different immich instances."""
|
||||
if not item.identifier:
|
||||
LOGGER.debug("Render all Immich instances")
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
@@ -97,7 +95,7 @@ class ImmichMediaSource(MediaSource):
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
for entry in self.entries
|
||||
for entry in entries
|
||||
]
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
entry: ImmichConfigEntry | None = (
|
||||
@@ -108,8 +106,22 @@ class ImmichMediaSource(MediaSource):
|
||||
assert entry
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
if identifier.album_id is None:
|
||||
# Get Albums
|
||||
if identifier.collection is None:
|
||||
LOGGER.debug("Render all collections for %s", entry.title)
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{identifier.unique_id}|albums",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title="albums",
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
)
|
||||
]
|
||||
|
||||
if identifier.collection_id is None:
|
||||
LOGGER.debug("Render all albums for %s", entry.title)
|
||||
try:
|
||||
albums = await immich_api.albums.async_get_all_albums()
|
||||
except ImmichError:
|
||||
@@ -118,80 +130,85 @@ class ImmichMediaSource(MediaSource):
|
||||
return [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=f"{item.identifier}/{album.album_id}",
|
||||
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
|
||||
media_class=MediaClass.DIRECTORY,
|
||||
media_content_type=MediaClass.IMAGE,
|
||||
title=album.name,
|
||||
title=album.album_name,
|
||||
can_play=False,
|
||||
can_expand=True,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.thumbnail_asset_id}/thumb.jpg/thumbnail",
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{album.album_thumbnail_asset_id}/thumbnail/image/jpg",
|
||||
)
|
||||
for album in albums
|
||||
]
|
||||
|
||||
# Request items of album
|
||||
LOGGER.debug(
|
||||
"Render all assets of album %s for %s",
|
||||
identifier.collection_id,
|
||||
entry.title,
|
||||
)
|
||||
try:
|
||||
album_info = await immich_api.albums.async_get_album_info(
|
||||
identifier.album_id
|
||||
identifier.collection_id
|
||||
)
|
||||
except ImmichError:
|
||||
return []
|
||||
|
||||
ret = [
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.IMAGE,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=False,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/{asset.file_name}/thumbnail",
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("image/")
|
||||
]
|
||||
ret: list[BrowseMediaSource] = []
|
||||
for asset in album_info.assets:
|
||||
if not (mime_type := asset.original_mime_type) or not mime_type.startswith(
|
||||
("image/", "video/")
|
||||
):
|
||||
continue
|
||||
|
||||
ret.extend(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}/"
|
||||
f"{identifier.album_id}/"
|
||||
f"{asset.asset_id}/"
|
||||
f"{asset.file_name}"
|
||||
),
|
||||
media_class=MediaClass.VIDEO,
|
||||
media_content_type=asset.mime_type,
|
||||
title=asset.file_name,
|
||||
can_play=True,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail.jpg/thumbnail",
|
||||
if mime_type.startswith("image/"):
|
||||
media_class = MediaClass.IMAGE
|
||||
can_play = False
|
||||
thumb_mime_type = mime_type
|
||||
else:
|
||||
media_class = MediaClass.VIDEO
|
||||
can_play = True
|
||||
thumb_mime_type = "image/jpeg"
|
||||
|
||||
ret.append(
|
||||
BrowseMediaSource(
|
||||
domain=DOMAIN,
|
||||
identifier=(
|
||||
f"{identifier.unique_id}|albums|"
|
||||
f"{identifier.collection_id}|"
|
||||
f"{asset.asset_id}|"
|
||||
f"{asset.original_file_name}|"
|
||||
f"{mime_type}"
|
||||
),
|
||||
media_class=media_class,
|
||||
media_content_type=mime_type,
|
||||
title=asset.original_file_name,
|
||||
can_play=can_play,
|
||||
can_expand=False,
|
||||
thumbnail=f"/immich/{identifier.unique_id}/{asset.asset_id}/thumbnail/{thumb_mime_type}",
|
||||
)
|
||||
)
|
||||
for asset in album_info.assets
|
||||
if asset.mime_type.startswith("video/")
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
if identifier.file_name is None:
|
||||
raise Unresolvable("No file name")
|
||||
mime_type, _ = mimetypes.guess_type(identifier.file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise Unresolvable("No file extension")
|
||||
try:
|
||||
identifier = ImmichMediaSourceIdentifier(item.identifier)
|
||||
except IndexError as err:
|
||||
raise Unresolvable(
|
||||
f"Could not parse identifier: {item.identifier}"
|
||||
) from err
|
||||
|
||||
if identifier.mime_type is None:
|
||||
raise Unresolvable(
|
||||
f"Could not resolve identifier that has no mime-type: {item.identifier}"
|
||||
)
|
||||
|
||||
return PlayMedia(
|
||||
(
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/{identifier.file_name}/fullsize"
|
||||
f"/immich/{identifier.unique_id}/{identifier.asset_id}/fullsize/{identifier.mime_type}"
|
||||
),
|
||||
mime_type,
|
||||
identifier.mime_type,
|
||||
)
|
||||
|
||||
|
||||
@@ -212,10 +229,10 @@ class ImmichMediaView(HomeAssistantView):
|
||||
if not self.hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
raise HTTPNotFound
|
||||
|
||||
asset_id, file_name, size = location.split("/")
|
||||
mime_type, _ = mimetypes.guess_type(file_name)
|
||||
if not isinstance(mime_type, str):
|
||||
raise HTTPNotFound
|
||||
try:
|
||||
asset_id, size, mime_type_base, mime_type_format = location.split("/")
|
||||
except ValueError as err:
|
||||
raise HTTPNotFound from err
|
||||
|
||||
entry: ImmichConfigEntry | None = (
|
||||
self.hass.config_entries.async_entry_for_domain_unique_id(
|
||||
@@ -226,7 +243,7 @@ class ImmichMediaView(HomeAssistantView):
|
||||
immich_api = entry.runtime_data.api
|
||||
|
||||
# stream response for videos
|
||||
if mime_type.startswith("video/"):
|
||||
if mime_type_base == "video":
|
||||
try:
|
||||
resp = await immich_api.assets.async_play_video_stream(asset_id)
|
||||
except ImmichError as exc:
|
||||
@@ -243,4 +260,4 @@ class ImmichMediaView(HomeAssistantView):
|
||||
image = await immich_api.assets.async_view_asset(asset_id, size)
|
||||
except ImmichError as exc:
|
||||
raise HTTPNotFound from exc
|
||||
return Response(body=image, content_type=mime_type)
|
||||
return Response(body=image, content_type=f"{mime_type_base}/{mime_type_format}")
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyiskra"],
|
||||
"requirements": ["pyiskra==0.1.15"]
|
||||
"requirements": ["pyiskra==0.1.19"]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .client_wrapper import CannotConnect, InvalidAuth, create_client, validate_input
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DOMAIN, PLATFORMS
|
||||
from .const import CONF_CLIENT_DEVICE_ID, DEFAULT_NAME, DOMAIN, PLATFORMS
|
||||
from .coordinator import JellyfinConfigEntry, JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -35,9 +35,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: JellyfinConfigEntry) ->
|
||||
coordinator = JellyfinDataUpdateCoordinator(
|
||||
hass, entry, client, server_info, user_id
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id,
|
||||
entry_type=dr.DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
entry.async_on_unload(client.stop)
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JellyfinDataUpdateCoordinator
|
||||
|
||||
|
||||
@@ -24,11 +24,7 @@ class JellyfinServerEntity(JellyfinEntity):
|
||||
"""Initialize the Jellyfin entity."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.server_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=coordinator.server_name,
|
||||
sw_version=coordinator.server_version,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,8 +11,9 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_MUTE,
|
||||
SERVICE_VOLUME_UP,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "keyboard"
|
||||
@@ -24,6 +25,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Listen for keyboard events."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Keyboard",
|
||||
},
|
||||
)
|
||||
|
||||
keyboard = PyKeyboard()
|
||||
keyboard.special_key_assignment()
|
||||
|
||||
@@ -20,8 +20,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from .const import DOMAIN
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=15)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=1)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=5)
|
||||
SETTINGS_UPDATE_INTERVAL = timedelta(hours=8)
|
||||
SCHEDULE_UPDATE_INTERVAL = timedelta(minutes=30)
|
||||
STATISTICS_UPDATE_INTERVAL = timedelta(minutes=15)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.6"]
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_on",
|
||||
translation_key="prebrew_time_on",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
@@ -158,7 +158,7 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
key="prebrew_off",
|
||||
translation_key="prebrew_time_off",
|
||||
device_class=NumberDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
native_step=PRECISION_TENTHS,
|
||||
native_min_value=0,
|
||||
native_max_value=10,
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["linkplay"],
|
||||
"requirements": ["python-linkplay==0.2.8"],
|
||||
"requirements": ["python-linkplay==0.2.9"],
|
||||
"zeroconf": ["_linkplay._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -7,8 +7,9 @@ import time
|
||||
import lirc
|
||||
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -26,6 +27,20 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the LIRC capability."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "LIRC",
|
||||
},
|
||||
)
|
||||
# blocking=True gives unexpected behavior (multiple responses for 1 press)
|
||||
# also by not blocking, we allow hass to shut down the thread gracefully
|
||||
# on exit.
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -967,33 +967,12 @@ DISCOVERY_SCHEMAS = [
|
||||
# don't discover this entry if the supported state list is empty
|
||||
secondary_value_is_not=[],
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MinPINCodeLength",
|
||||
translation_key="min_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MinPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="MaxPINCodeLength",
|
||||
translation_key="max_pin_code_length",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=None,
|
||||
),
|
||||
entity_class=MatterSensor,
|
||||
required_attributes=(clusters.DoorLock.Attributes.MaxPINCodeLength,),
|
||||
),
|
||||
MatterDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
entity_description=MatterSensorEntityDescription(
|
||||
key="TargetPositionLiftPercent100ths",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
translation_key="window_covering_target_position",
|
||||
measurement_to_ha=lambda x: round((10000 - x) / 100),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
||||
@@ -390,12 +390,6 @@
|
||||
"evse_user_max_charge_current": {
|
||||
"name": "User max charge current"
|
||||
},
|
||||
"min_pin_code_length": {
|
||||
"name": "Min PIN code length"
|
||||
},
|
||||
"max_pin_code_length": {
|
||||
"name": "Max PIN code length"
|
||||
},
|
||||
"window_covering_target_position": {
|
||||
"name": "Target opening position"
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ from homeassistant.components.light import (
|
||||
from homeassistant.components.sensor import (
|
||||
CONF_STATE_CLASS,
|
||||
DEVICE_CLASS_UNITS,
|
||||
STATE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
@@ -640,6 +641,13 @@ def validate_sensor_platform_config(
|
||||
):
|
||||
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
|
||||
|
||||
|
||||
@@ -676,11 +684,19 @@ class PlatformField:
|
||||
@callback
|
||||
def unit_of_measurement_selector(user_data: dict[str, Any | None]) -> 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 (
|
||||
user_data is None
|
||||
or (device_class := user_data.get(CONF_DEVICE_CLASS)) is None
|
||||
or device_class not in DEVICE_CLASS_UNITS
|
||||
):
|
||||
device_class := user_data.get(CONF_DEVICE_CLASS)
|
||||
) is None or device_class not in DEVICE_CLASS_UNITS:
|
||||
return TEXT_SELECTOR
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
|
||||
@@ -14,6 +14,7 @@ from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_UNITS,
|
||||
DEVICE_CLASSES_SCHEMA,
|
||||
ENTITY_ID_FORMAT,
|
||||
STATE_CLASS_UNITS,
|
||||
STATE_CLASSES_SCHEMA,
|
||||
RestoreSensor,
|
||||
SensorDeviceClass,
|
||||
@@ -117,6 +118,17 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
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 (
|
||||
unit_of_measurement := config.get(CONF_UNIT_OF_MEASUREMENT)
|
||||
) is None:
|
||||
|
||||
@@ -644,6 +644,7 @@
|
||||
"invalid_template": "Invalid template",
|
||||
"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_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",
|
||||
"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",
|
||||
|
||||
@@ -446,4 +446,5 @@ class NestFlowHandler(
|
||||
self, discovery_info: DhcpServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by discovery."""
|
||||
await self._async_handle_discovery_without_unique_id()
|
||||
return await self.async_step_user()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"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]
|
||||
|
||||
MIN_TEMPERATURE = 7
|
||||
MAX_TEMPERATURE = 40
|
||||
MAX_TEMPERATURE = 30
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.12.2"]
|
||||
"requirements": ["opower==0.12.3"]
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
"""The pandora component."""
|
||||
|
||||
DOMAIN = "pandora"
|
||||
|
||||
@@ -27,10 +27,13 @@ from homeassistant.const import (
|
||||
SERVICE_VOLUME_DOWN,
|
||||
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.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -53,6 +56,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""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():
|
||||
return
|
||||
pandora = PandoraMediaPlayer("Pandora")
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/picnic",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_picnic_api2"],
|
||||
"requirements": ["python-picnic-api2==1.2.4"]
|
||||
"requirements": ["python-picnic-api2==1.3.1"]
|
||||
}
|
||||
|
||||
@@ -15,5 +15,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyprobeplus==1.0.0"]
|
||||
"requirements": ["pyprobeplus==1.0.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ical"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["ical==9.2.5"]
|
||||
"requirements": ["ical==10.0.0"]
|
||||
}
|
||||
|
||||
@@ -150,6 +150,10 @@ async def async_setup_entry(
|
||||
|
||||
if host.api.new_devices and config_entry.state == ConfigEntryState.LOADED:
|
||||
# Their are new cameras/chimes connected, reload to add them.
|
||||
_LOGGER.debug(
|
||||
"Reloading Reolink %s to add new device (capabilities)",
|
||||
host.api.nvr_name,
|
||||
)
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_reload(config_entry.entry_id)
|
||||
)
|
||||
|
||||
@@ -194,6 +194,13 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
if existing_entry and existing_entry.data[CONF_HOST] != discovery_info.ip:
|
||||
_LOGGER.debug(
|
||||
"Reolink DHCP reported new IP '%s', updating from old IP '%s'",
|
||||
discovery_info.ip,
|
||||
existing_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
|
||||
|
||||
self.context["title_placeholders"] = {
|
||||
|
||||
@@ -19,5 +19,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.13.3"]
|
||||
"requirements": ["reolink-aio==0.13.5"]
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
verify_ssl=False,
|
||||
ssl_cipher=SSLCipherList.INSECURE,
|
||||
)
|
||||
self._vod_type: str | None = None
|
||||
|
||||
async def get(
|
||||
self,
|
||||
@@ -68,6 +69,8 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
|
||||
filename_decoded = urlsafe_b64decode(filename.encode("utf-8")).decode("utf-8")
|
||||
ch = int(channel)
|
||||
if self._vod_type is not None:
|
||||
vod_type = self._vod_type
|
||||
try:
|
||||
host = get_host(self.hass, config_entry_id)
|
||||
except Unresolvable:
|
||||
@@ -127,6 +130,25 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
"apolication/octet-stream",
|
||||
]:
|
||||
err_str = f"Reolink playback expected video/mp4 but got {reolink_response.content_type}"
|
||||
if (
|
||||
reolink_response.content_type == "video/x-flv"
|
||||
and vod_type == VodRequestType.PLAYBACK.value
|
||||
):
|
||||
# next time use DOWNLOAD immediately
|
||||
self._vod_type = VodRequestType.DOWNLOAD.value
|
||||
_LOGGER.debug(
|
||||
"%s, retrying using download instead of playback cmd", err_str
|
||||
)
|
||||
return await self.get(
|
||||
request,
|
||||
config_entry_id,
|
||||
channel,
|
||||
stream_res,
|
||||
self._vod_type,
|
||||
filename,
|
||||
retry,
|
||||
)
|
||||
|
||||
_LOGGER.error(err_str)
|
||||
if reolink_response.content_type == "text/html":
|
||||
text = await reolink_response.text()
|
||||
@@ -140,7 +162,10 @@ class PlaybackProxyView(HomeAssistantView):
|
||||
reolink_response.reason,
|
||||
response_headers,
|
||||
)
|
||||
response_headers["Content-Type"] = "video/mp4"
|
||||
if "Content-Type" not in response_headers:
|
||||
response_headers["Content-Type"] = reolink_response.content_type
|
||||
if response_headers["Content-Type"] == "apolication/octet-stream":
|
||||
response_headers["Content-Type"] = "application/octet-stream"
|
||||
|
||||
response = web.StreamResponse(
|
||||
status=reolink_response.status,
|
||||
|
||||
@@ -636,14 +636,21 @@ class SamsungTVWSBridge(
|
||||
)
|
||||
self._remote = None
|
||||
except ConnectionFailure as err:
|
||||
LOGGER.warning(
|
||||
(
|
||||
error_details = err.args[0]
|
||||
if "ms.channel.timeOut" in (error_details := repr(err)):
|
||||
# The websocket was connected, but the TV is probably asleep
|
||||
LOGGER.debug(
|
||||
"Channel timeout occurred trying to get remote for %s: %s",
|
||||
self.host,
|
||||
error_details,
|
||||
)
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"Unexpected ConnectionFailure trying to get remote for %s, "
|
||||
"please report this issue: %s"
|
||||
),
|
||||
self.host,
|
||||
repr(err),
|
||||
)
|
||||
"please report this issue: %s",
|
||||
self.host,
|
||||
error_details,
|
||||
)
|
||||
self._remote = None
|
||||
except (WebSocketException, AsyncioTimeoutError, OSError) as err:
|
||||
LOGGER.debug("Failed to get remote for %s: %s", self.host, repr(err))
|
||||
|
||||
@@ -39,7 +39,7 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
|
||||
self.bridge = bridge
|
||||
self.is_on: bool | None = False
|
||||
self.is_on: bool | None = None
|
||||
self.async_extra_update: Callable[[], Coroutine[Any, Any, None]] | None = None
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
@@ -52,7 +52,12 @@ class SamsungTVDataUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
else:
|
||||
self.is_on = await self.bridge.async_is_on()
|
||||
if self.is_on != old_state:
|
||||
LOGGER.debug("TV %s state updated to %s", self.bridge.host, self.is_on)
|
||||
LOGGER.debug(
|
||||
"TV %s state updated from %s to %s",
|
||||
self.bridge.host,
|
||||
old_state,
|
||||
self.is_on,
|
||||
)
|
||||
|
||||
if self.async_extra_update:
|
||||
await self.async_extra_update()
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
"name": "State class",
|
||||
"state": {
|
||||
"measurement": "Measurement",
|
||||
"measurement_angle": "Measurement Angle",
|
||||
"measurement_angle": "Measurement angle",
|
||||
"total": "Total",
|
||||
"total_increasing": "Total increasing"
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ from dataclasses import dataclass
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY, RPC_GENERATIONS
|
||||
from aioshelly.const import BLU_TRV_IDENTIFIER, MODEL_BLU_GATEWAY_G3, RPC_GENERATIONS
|
||||
from aioshelly.exceptions import DeviceConnectionError, InvalidAuthError, RpcCallError
|
||||
|
||||
from homeassistant.components.button import (
|
||||
@@ -62,7 +62,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="self_test",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
press_action="trigger_shelly_gas_self_test",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="mute",
|
||||
@@ -70,7 +70,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="mute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_mute",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
ShellyButtonDescription[ShellyBlockCoordinator](
|
||||
key="unmute",
|
||||
@@ -78,7 +78,7 @@ BUTTONS: Final[list[ShellyButtonDescription[Any]]] = [
|
||||
translation_key="unmute",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_shelly_gas_unmute",
|
||||
supported=lambda coordinator: coordinator.device.model in SHELLY_GAS_MODELS,
|
||||
supported=lambda coordinator: coordinator.model in SHELLY_GAS_MODELS,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -89,7 +89,7 @@ BLU_TRV_BUTTONS: Final[list[ShellyButtonDescription]] = [
|
||||
translation_key="calibrate",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
press_action="trigger_blu_trv_calibration",
|
||||
supported=lambda coordinator: coordinator.device.model == MODEL_BLU_GATEWAY,
|
||||
supported=lambda coordinator: coordinator.model == MODEL_BLU_GATEWAY_G3,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -160,6 +160,7 @@ async def async_setup_entry(
|
||||
ShellyBluTrvButton(coordinator, button, id_)
|
||||
for id_ in blutrv_key_ids
|
||||
for button in BLU_TRV_BUTTONS
|
||||
if button.supported(coordinator)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -218,5 +218,6 @@ class SmaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
}
|
||||
),
|
||||
description_placeholders={CONF_HOST: self._data[CONF_HOST]},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -32,6 +32,16 @@
|
||||
},
|
||||
"description": "Enter your SMA device information.",
|
||||
"title": "Set up SMA Solar"
|
||||
},
|
||||
"discovery_confirm": {
|
||||
"title": "[%key:component::sma::config::step::user::title%]",
|
||||
"description": "Do you want to set up the discovered SMA device ({host})?",
|
||||
"data": {
|
||||
"group": "[%key:component::sma::config::step::user::data::group%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.4"],
|
||||
"requirements": ["pysmlight==0.2.5"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -6,9 +6,14 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE, CONF_NAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import (
|
||||
@@ -41,6 +46,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
DEPRECATED_ISSUE_ID = f"deprecated_system_packages_config_flow_integration_{DOMAIN}"
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -52,6 +58,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Configure Gammu state machine."""
|
||||
async_create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
DEPRECATED_ISSUE_ID,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_config_flow_integration",
|
||||
translation_placeholders={
|
||||
"integration_title": "SMS notifications via GSM-modem",
|
||||
},
|
||||
)
|
||||
|
||||
device = entry.data[CONF_DEVICE]
|
||||
connection_mode = "at"
|
||||
@@ -101,4 +120,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
gateway = hass.data[DOMAIN].pop(SMS_GATEWAY)[GATEWAY]
|
||||
await gateway.terminate_async()
|
||||
|
||||
if not hass.config_entries.async_loaded_entries(DOMAIN):
|
||||
async_delete_issue(hass, HOMEASSISTANT_DOMAIN, DEPRECATED_ISSUE_ID)
|
||||
|
||||
return unload_ok
|
||||
|
||||
@@ -7,8 +7,13 @@ import logging
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
ServiceCall,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv, intent
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "snips"
|
||||
@@ -91,6 +96,20 @@ SERVICE_SCHEMA_FEEDBACK = vol.Schema(
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Activate Snips component."""
|
||||
async_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": "Snips",
|
||||
},
|
||||
)
|
||||
|
||||
# Make sure MQTT integration is enabled and the client is available
|
||||
if not await mqtt.async_wait_for_mqtt_client(hass):
|
||||
|
||||
@@ -367,7 +367,9 @@ class SwitchbotOptionsFlowHandler(OptionsFlow):
|
||||
),
|
||||
): int
|
||||
}
|
||||
if self.config_entry.data.get(CONF_SENSOR_TYPE) == SupportedModels.LOCK_PRO:
|
||||
if self.config_entry.data.get(CONF_SENSOR_TYPE, "").startswith(
|
||||
SupportedModels.LOCK
|
||||
):
|
||||
options.update(
|
||||
{
|
||||
vol.Optional(
|
||||
|
||||
@@ -7,7 +7,13 @@ from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
|
||||
from aiohttp import web
|
||||
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
|
||||
from switchbot_api import (
|
||||
Device,
|
||||
Remote,
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
|
||||
from homeassistant.components import webhook
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -175,12 +181,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
api = SwitchBotAPI(token=token, secret=secret)
|
||||
try:
|
||||
devices = await api.list_devices()
|
||||
except InvalidAuth as ex:
|
||||
except SwitchBotAuthenticationError as ex:
|
||||
_LOGGER.error(
|
||||
"Invalid authentication while connecting to SwitchBot API: %s", ex
|
||||
)
|
||||
return False
|
||||
except CannotConnect as ex:
|
||||
except SwitchBotConnectionError as ex:
|
||||
raise ConfigEntryNotReady from ex
|
||||
_LOGGER.debug("Devices: %s", devices)
|
||||
coordinators_by_id: dict[str, SwitchBotCoordinator] = {}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI
|
||||
from switchbot_api import (
|
||||
SwitchBotAPI,
|
||||
SwitchBotAuthenticationError,
|
||||
SwitchBotConnectionError,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
@@ -36,9 +40,9 @@ class SwitchBotCloudConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
await SwitchBotAPI(
|
||||
token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY]
|
||||
).list_devices()
|
||||
except CannotConnect:
|
||||
except SwitchBotConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
except SwitchBotAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
||||
@@ -4,7 +4,7 @@ from asyncio import timeout
|
||||
from logging import getLogger
|
||||
from typing import Any
|
||||
|
||||
from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI
|
||||
from switchbot_api import Device, Remote, SwitchBotAPI, SwitchBotConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -70,5 +70,5 @@ class SwitchBotCoordinator(DataUpdateCoordinator[Status]):
|
||||
status: Status = await self._api.get_status(self._device_id)
|
||||
_LOGGER.debug("Refreshing %s with %s", self._device_id, status)
|
||||
return status
|
||||
except CannotConnect as err:
|
||||
except SwitchBotConnectionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["switchbot_api"],
|
||||
"requirements": ["switchbot-api==2.3.1"]
|
||||
"requirements": ["switchbot-api==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=4)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_MOBILE_DEVICE_INTERVAL = timedelta(minutes=5)
|
||||
|
||||
|
||||
class TadoDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict]]):
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiotedee"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiotedee==0.2.20"]
|
||||
"requirements": ["aiotedee==0.2.23"]
|
||||
}
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
"""The tensorflow component."""
|
||||
|
||||
DOMAIN = "tensorflow"
|
||||
CONF_GRAPH = "graph"
|
||||
|
||||
@@ -26,15 +26,21 @@ from homeassistant.const import (
|
||||
CONF_SOURCE,
|
||||
EVENT_HOMEASSISTANT_START,
|
||||
)
|
||||
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, template
|
||||
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.util.pil import draw_box
|
||||
|
||||
from . import CONF_GRAPH, DOMAIN
|
||||
|
||||
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
|
||||
|
||||
DOMAIN = "tensorflow"
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MATCHES = "matches"
|
||||
@@ -47,7 +53,6 @@ CONF_BOTTOM = "bottom"
|
||||
CONF_CATEGORIES = "categories"
|
||||
CONF_CATEGORY = "category"
|
||||
CONF_FILE_OUT = "file_out"
|
||||
CONF_GRAPH = "graph"
|
||||
CONF_LABELS = "labels"
|
||||
CONF_LABEL_OFFSET = "label_offset"
|
||||
CONF_LEFT = "left"
|
||||
@@ -110,6 +115,21 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the TensorFlow image processing 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": "Tensorflow",
|
||||
},
|
||||
)
|
||||
|
||||
model_config = config[CONF_MODEL]
|
||||
model_dir = model_config.get(CONF_MODEL_DIR) or hass.config.path("tensorflow")
|
||||
labels = model_config.get(CONF_LABELS) or hass.config.path(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.17"]
|
||||
"requirements": ["tesla-fleet-api==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -125,6 +125,9 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryBinarySensorEntityDescription, ...] = (
|
||||
key="charge_state_conn_charge_cable",
|
||||
polling=True,
|
||||
polling_value_fn=lambda x: x != "<invalid>",
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_ChargingCableType(
|
||||
lambda value: callback(value != "Unknown")
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==1.0.17", "teslemetry-stream==0.7.9"]
|
||||
"requirements": ["tesla-fleet-api==1.1.1", "teslemetry-stream==0.7.9"]
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
key="charge_state_charging_state",
|
||||
polling=True,
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_DetailedChargeState(
|
||||
lambda value: None if value is None else callback(value.lower())
|
||||
lambda value: callback(None if value is None else CHARGE_STATES.get(value))
|
||||
),
|
||||
polling_value_fn=lambda value: CHARGE_STATES.get(str(value)),
|
||||
options=list(CHARGE_STATES.values()),
|
||||
@@ -533,7 +533,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslemetryVehicleSensorEntityDescription, ...] = (
|
||||
TeslemetryVehicleSensorEntityDescription(
|
||||
key="bms_state",
|
||||
streaming_listener=lambda vehicle, callback: vehicle.listen_BMSState(
|
||||
lambda value: None if value is None else callback(BMS_STATES.get(value))
|
||||
lambda value: callback(None if value is None else BMS_STATES.get(value))
|
||||
),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(BMS_STATES.values()),
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.0.17"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ STATES = {
|
||||
"Stopped": MediaPlayerState.IDLE,
|
||||
}
|
||||
|
||||
# Tesla uses 31 steps, in 0.333 increments up to 10.333
|
||||
VOLUME_STEP = 1 / 31
|
||||
VOLUME_FACTOR = 31 / 3 # 10.333
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -38,6 +42,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
|
||||
"""Vehicle Location Media Class."""
|
||||
|
||||
_attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
_attr_volume_step = VOLUME_STEP
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -57,9 +62,7 @@ class TessieMediaEntity(TessieEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def volume_level(self) -> float:
|
||||
"""Volume level of the media player (0..1)."""
|
||||
return self.get("vehicle_state_media_info_audio_volume", 0) / self.get(
|
||||
"vehicle_state_media_info_audio_volume_max", 10.333333
|
||||
)
|
||||
return self.get("vehicle_state_media_info_audio_volume", 0) / VOLUME_FACTOR
|
||||
|
||||
@property
|
||||
def media_duration(self) -> int | None:
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["uiprotect", "unifi_discovery"],
|
||||
"requirements": ["uiprotect==7.10.0", "unifi-discovery==1.2.0"],
|
||||
"requirements": ["uiprotect==7.10.1", "unifi-discovery==1.2.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -300,7 +300,9 @@ async def handle_call_service(
|
||||
translation_placeholders=err.translation_placeholders,
|
||||
)
|
||||
except HomeAssistantError as err:
|
||||
connection.logger.exception("Unexpected exception")
|
||||
connection.logger.error(
|
||||
"Error during service call to %s.%s: %s", msg["domain"], msg["service"], err
|
||||
)
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
const.ERR_HOME_ASSISTANT_ERROR,
|
||||
|
||||
@@ -94,21 +94,59 @@ def _get_obj_holidays(
|
||||
language=language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
supported_languages = obj_holidays.supported_languages
|
||||
default_language = obj_holidays.default_language
|
||||
|
||||
if default_language and not language:
|
||||
# If no language is set, use the default language
|
||||
LOGGER.debug("Changing language from None to %s", default_language)
|
||||
return country_holidays( # Return default if no language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
if (
|
||||
(supported_languages := obj_holidays.supported_languages)
|
||||
default_language
|
||||
and language
|
||||
and language not in supported_languages
|
||||
and language.startswith("en")
|
||||
):
|
||||
# If language does not match supported languages, use the first English variant
|
||||
if default_language.startswith("en"):
|
||||
LOGGER.debug("Changing language from %s to %s", language, default_language)
|
||||
return country_holidays( # Return default English if default language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
for lang in supported_languages:
|
||||
if lang.startswith("en"):
|
||||
obj_holidays = country_holidays(
|
||||
LOGGER.debug("Changing language from %s to %s", language, lang)
|
||||
return country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=lang,
|
||||
categories=set_categories,
|
||||
)
|
||||
LOGGER.debug("Changing language from %s to %s", language, lang)
|
||||
|
||||
if default_language and language and language not in supported_languages:
|
||||
# If language does not match supported languages, use the default language
|
||||
LOGGER.debug("Changing language from %s to %s", language, default_language)
|
||||
return country_holidays( # Return default English if default language
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=default_language,
|
||||
categories=set_categories,
|
||||
)
|
||||
|
||||
return obj_holidays
|
||||
|
||||
|
||||
|
||||
@@ -67,8 +67,7 @@ def add_province_and_language_to_schema(
|
||||
|
||||
_country = country_holidays(country=country)
|
||||
if country_default_language := (_country.default_language):
|
||||
selectable_languages = _country.supported_languages
|
||||
new_selectable_languages = list(selectable_languages)
|
||||
new_selectable_languages = list(_country.supported_languages)
|
||||
language_schema = {
|
||||
vol.Optional(
|
||||
CONF_LANGUAGE, default=country_default_language
|
||||
@@ -154,19 +153,7 @@ def validate_custom_dates(user_input: dict[str, Any]) -> None:
|
||||
years=year,
|
||||
language=language,
|
||||
)
|
||||
if (
|
||||
(supported_languages := obj_holidays.supported_languages)
|
||||
and language
|
||||
and language.startswith("en")
|
||||
):
|
||||
for lang in supported_languages:
|
||||
if lang.startswith("en"):
|
||||
obj_holidays = country_holidays(
|
||||
country,
|
||||
subdiv=province,
|
||||
years=year,
|
||||
language=lang,
|
||||
)
|
||||
|
||||
else:
|
||||
obj_holidays = HolidayBase(years=year)
|
||||
|
||||
|
||||
@@ -94,6 +94,7 @@ from .const import (
|
||||
CONF_DATA_COLLECTION_OPTED_IN,
|
||||
CONF_INSTALLER_MODE,
|
||||
CONF_INTEGRATION_CREATED_ADDON,
|
||||
CONF_KEEP_OLD_DEVICES,
|
||||
CONF_LR_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_LR_S2_AUTHENTICATED_KEY,
|
||||
CONF_NETWORK_KEY,
|
||||
@@ -405,9 +406,10 @@ class DriverEvents:
|
||||
|
||||
# Devices that are in the device registry that are not known by the controller
|
||||
# can be removed
|
||||
for device in stored_devices:
|
||||
if device not in known_devices and device not in provisioned_devices:
|
||||
self.dev_reg.async_remove_device(device.id)
|
||||
if not self.config_entry.data.get(CONF_KEEP_OLD_DEVICES):
|
||||
for device in stored_devices:
|
||||
if device not in known_devices and device not in provisioned_devices:
|
||||
self.dev_reg.async_remove_device(device.id)
|
||||
|
||||
# run discovery on controller node
|
||||
if controller.own_node:
|
||||
|
||||
@@ -56,6 +56,7 @@ from .const import (
|
||||
CONF_ADDON_S2_AUTHENTICATED_KEY,
|
||||
CONF_ADDON_S2_UNAUTHENTICATED_KEY,
|
||||
CONF_INTEGRATION_CREATED_ADDON,
|
||||
CONF_KEEP_OLD_DEVICES,
|
||||
CONF_LR_S2_ACCESS_CONTROL_KEY,
|
||||
CONF_LR_S2_AUTHENTICATED_KEY,
|
||||
CONF_S0_LEGACY_KEY,
|
||||
@@ -170,8 +171,6 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
_title: str
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Set up flow instance."""
|
||||
self.s0_legacy_key: str | None = None
|
||||
@@ -446,7 +445,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
# at least for a short time.
|
||||
return self.async_abort(reason="already_in_progress")
|
||||
if current_config_entries := self._async_current_entries(include_ignore=False):
|
||||
config_entry = next(
|
||||
self._reconfigure_config_entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in current_config_entries
|
||||
@@ -454,7 +453,7 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not config_entry:
|
||||
if not self._reconfigure_config_entry:
|
||||
return self.async_abort(reason="addon_required")
|
||||
|
||||
vid = discovery_info.vid
|
||||
@@ -503,31 +502,9 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
title = human_name.split(" - ")[0].strip()
|
||||
self.context["title_placeholders"] = {CONF_NAME: title}
|
||||
self._title = title
|
||||
return await self.async_step_usb_confirm()
|
||||
|
||||
async def async_step_usb_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle USB Discovery confirmation."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="usb_confirm",
|
||||
description_placeholders={CONF_NAME: self._title},
|
||||
)
|
||||
|
||||
self._usb_discovery = True
|
||||
if current_config_entries := self._async_current_entries(include_ignore=False):
|
||||
self._reconfigure_config_entry = next(
|
||||
(
|
||||
entry
|
||||
for entry in current_config_entries
|
||||
if entry.data.get(CONF_USE_ADDON)
|
||||
),
|
||||
None,
|
||||
)
|
||||
if not self._reconfigure_config_entry:
|
||||
return self.async_abort(reason="addon_required")
|
||||
if current_config_entries:
|
||||
return await self.async_step_intent_migrate()
|
||||
|
||||
return await self.async_step_installation_type()
|
||||
@@ -1407,9 +1384,20 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry = self._reconfigure_config_entry
|
||||
assert config_entry is not None
|
||||
|
||||
# Make sure we keep the old devices
|
||||
# so that user customizations are not lost,
|
||||
# when loading the config entry.
|
||||
self.hass.config_entries.async_update_entry(
|
||||
config_entry, data=config_entry.data | {CONF_KEEP_OLD_DEVICES: True}
|
||||
)
|
||||
|
||||
# Reload the config entry to reconnect the client after the addon restart
|
||||
await self.hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
data = config_entry.data.copy()
|
||||
data.pop(CONF_KEEP_OLD_DEVICES, None)
|
||||
self.hass.config_entries.async_update_entry(config_entry, data=data)
|
||||
|
||||
@callback
|
||||
def forward_progress(event: dict) -> None:
|
||||
"""Forward progress events to frontend."""
|
||||
@@ -1460,6 +1448,15 @@ class ZWaveJSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
config_entry, unique_id=str(version_info.home_id)
|
||||
)
|
||||
await self.hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
# Reload the config entry two times to clean up
|
||||
# the stale device entry.
|
||||
# Since both the old and the new controller have the same node id,
|
||||
# but different hardware identifiers, the integration
|
||||
# will create a new device for the new controller, on the first reload,
|
||||
# but not immediately remove the old device.
|
||||
await self.hass.config_entries.async_reload(config_entry.entry_id)
|
||||
|
||||
finally:
|
||||
for unsub in unsubs:
|
||||
unsub()
|
||||
|
||||
@@ -27,6 +27,7 @@ CONF_ADDON_LR_S2_ACCESS_CONTROL_KEY = "lr_s2_access_control_key"
|
||||
CONF_ADDON_LR_S2_AUTHENTICATED_KEY = "lr_s2_authenticated_key"
|
||||
CONF_INSTALLER_MODE = "installer_mode"
|
||||
CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon"
|
||||
CONF_KEEP_OLD_DEVICES = "keep_old_devices"
|
||||
CONF_NETWORK_KEY = "network_key"
|
||||
CONF_S0_LEGACY_KEY = "s0_legacy_key"
|
||||
CONF_S2_ACCESS_CONTROL_KEY = "s2_access_control_key"
|
||||
|
||||
@@ -98,9 +98,6 @@
|
||||
"start_addon": {
|
||||
"title": "The Z-Wave add-on is starting."
|
||||
},
|
||||
"usb_confirm": {
|
||||
"description": "Do you want to set up {name} with the Z-Wave add-on?"
|
||||
},
|
||||
"zeroconf_confirm": {
|
||||
"description": "Do you want to add the Z-Wave Server with home ID {home_id} found at {url} to Home Assistant?",
|
||||
"title": "Discovered Z-Wave Server"
|
||||
@@ -134,7 +131,7 @@
|
||||
},
|
||||
"installation_type": {
|
||||
"title": "Set up Z-Wave",
|
||||
"description": "Choose the installation type for your Z-Wave integration.",
|
||||
"description": "In a few steps, we’re going to set up your Home Assistant Connect ZWA-2. Home Assistant can automatically install and configure the recommended Z-Wave setup, or you can customize it.",
|
||||
"menu_options": {
|
||||
"intent_recommended": "Recommended installation",
|
||||
"intent_custom": "Custom installation"
|
||||
|
||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2025
|
||||
MINOR_VERSION: Final = 6
|
||||
PATCH_VERSION: Final = "0.dev0"
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)
|
||||
|
||||
@@ -543,8 +543,17 @@ class FlowManager(abc.ABC, Generic[_FlowContextT, _FlowResultT, _HandlerT]):
|
||||
flow.cur_step = result
|
||||
return result
|
||||
|
||||
# We pass a copy of the result because we're mutating our version
|
||||
result = await self.async_finish_flow(flow, result.copy())
|
||||
try:
|
||||
# We pass a copy of the result because we're mutating our version
|
||||
result = await self.async_finish_flow(flow, result.copy())
|
||||
except AbortFlow as err:
|
||||
result = self._flow_result(
|
||||
type=FlowResultType.ABORT,
|
||||
flow_id=flow.flow_id,
|
||||
handler=flow.handler,
|
||||
reason=err.reason,
|
||||
description_placeholders=err.description_placeholders,
|
||||
)
|
||||
|
||||
# _async_finish_flow may change result type, check it again
|
||||
if result["type"] == FlowResultType.FORM:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user