Compare commits

..

12 Commits

Author SHA1 Message Date
J. Nick Koston
162c27b92c Merge branch 'dev' into block_pyserial_asyncio 2025-05-26 10:40:34 -05:00
J. Nick Koston
e7e42dc318 Merge branch 'dev' into block_pyserial_asyncio 2024-08-16 17:48:47 -05:00
J. Nick Koston
9aa288ed44 Merge branch 'dev' into block_pyserial_asyncio 2024-06-22 16:36:10 -05:00
J. Nick Koston
5aacb6e1b8 Merge branch 'dev' into block_pyserial_asyncio 2024-06-22 15:18:40 -05:00
J. Nick Koston
1428ce4084 Merge branch 'dev' into block_pyserial_asyncio 2024-05-05 10:06:24 -05:00
J. Nick Koston
dba07ac90d Merge branch 'dev' into block_pyserial_asyncio 2024-05-03 02:12:40 -05:00
J. Nick Koston
264df97069 Merge branch 'dev' into block_pyserial_asyncio 2024-05-02 16:37:09 -05:00
J. Nick Koston
c3f493394a Merge branch 'drop_pyserial_zha' into block_pyserial_asyncio 2024-05-02 11:10:42 -05:00
J. Nick Koston
7e3e82746f Drop pyserial-asyncio from zha
This may not be possible yet, but the long term goal is to get
rid of pyserial-asyncio everywhere so we can prevent future
integrations from using it and than we have to go though the
effort of getting them to replace it with pyserial-asyncio-fast
to avoid the event loop being blocked

needed for https://github.com/home-assistant/core/pull/116635
2024-05-02 11:09:43 -05:00
J. Nick Koston
33724240d7 Merge branch 'serial' into block_pyserial_asyncio 2024-05-02 11:06:25 -05:00
J. Nick Koston
998a4eab9e Replace pyserial-asyncio with pyserial-asyncio-fast in serial
pyserial-asyncio is unmantained and does blocking I/O in the event loop
2024-05-02 11:04:45 -05:00
J. Nick Koston
9985262a53 Block pyserial-asyncio in favor of pyserial-asyncio-fast
pyserial-asyncio does blocking I/O in asyncio loop and is not maintained
2024-05-02 11:00:12 -05:00
1080 changed files with 3195 additions and 23906 deletions

View File

@@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
homeassistant.components.alarm_control_panel.*
homeassistant.components.alert.*
homeassistant.components.alexa.*
homeassistant.components.alexa_devices.*
homeassistant.components.alpha_vantage.*
homeassistant.components.amazon_devices.*
homeassistant.components.amazon_polly.*
homeassistant.components.amberelectric.*
homeassistant.components.ambient_network.*

4
CODEOWNERS generated
View File

@@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
/tests/components/alert/ @home-assistant/core @frenck
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
/homeassistant/components/alexa_devices/ @chemelli74
/tests/components/alexa_devices/ @chemelli74
/homeassistant/components/amazon_devices/ @chemelli74
/tests/components/amazon_devices/ @chemelli74
/homeassistant/components/amazon_polly/ @jschlyter
/homeassistant/components/amberelectric/ @madpilot
/tests/components/amberelectric/ @madpilot

View File

@@ -3,7 +3,7 @@
"name": "Amazon",
"integrations": [
"alexa",
"alexa_devices",
"amazon_devices",
"amazon_polly",
"aws",
"aws_s3",

View File

@@ -1,6 +0,0 @@
{
"domain": "shelly",
"name": "shelly",
"integrations": ["shelly"],
"iot_standards": ["zwave"]
}

View File

@@ -40,10 +40,9 @@ 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 = [
hubs: list[aiopulse.Hub] = [
hub
async for hub in aiopulse.Hub.discover()
if hub.id not in already_configured

View File

@@ -51,16 +51,9 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
async def _async_setup(self) -> None:
"""Set up the coordinator."""
try:
self._current_version = (
await self.client.get_current_measures()
).firmware_version
except AirGradientError as error:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": str(error)},
) from error
self._current_version = (
await self.client.get_current_measures()
).firmware_version
async def _async_update_data(self) -> AirGradientData:
try:

View File

@@ -1,66 +0,0 @@
"""Diagnostics support for Alexa Devices integration."""
from __future__ import annotations
from typing import Any
from aioamazondevices.api import AmazonDevice
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from .coordinator import AmazonConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
devices: list[dict[str, dict[str, Any]]] = [
build_device_data(device) for device in coordinator.data.values()
]
return {
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
"device_info": {
"last_update success": coordinator.last_update_success,
"last_exception": repr(coordinator.last_exception),
"devices": devices,
},
}
async def async_get_device_diagnostics(
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = entry.runtime_data
assert device_entry.serial_number
return build_device_data(coordinator.data[device_entry.serial_number])
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
"""Build device data for diagnostics."""
return {
"account name": device.account_name,
"capabilities": device.capabilities,
"device family": device.device_family,
"device type": device.device_type,
"device cluster members": device.device_cluster_members,
"online": device.online,
"serial number": device.serial_number,
"software version": device.software_version,
"do not disturb": device.do_not_disturb,
"response style": device.response_style,
"bluetooth state": device.bluetooth_state,
}

View File

@@ -1,12 +0,0 @@
{
"domain": "alexa_devices",
"name": "Alexa Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==3.1.2"]
}

View File

@@ -1,4 +1,4 @@
"""Alexa Devices integration."""
"""Amazon Devices integration."""
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -13,7 +13,7 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Set up Alexa Devices platform."""
"""Set up Amazon Devices platform."""
coordinator = AmazonDevicesCoordinator(hass, entry)

View File

@@ -13,7 +13,6 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -26,7 +25,7 @@ PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Alexa Devices binary sensor entity description."""
"""Amazon Devices binary sensor entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
@@ -35,12 +34,10 @@ BINARY_SENSORS: Final = (
AmazonBinarySensorEntityDescription(
key="online",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda _device: _device.online,
),
AmazonBinarySensorEntityDescription(
key="bluetooth",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="bluetooth",
is_on_fn=lambda _device: _device.bluetooth_state,
),
@@ -52,7 +49,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Alexa Devices binary sensors based on a config entry."""
"""Set up Amazon Devices binary sensors based on a config entry."""
coordinator = entry.runtime_data

View File

@@ -1,4 +1,4 @@
"""Config flow for Alexa Devices integration."""
"""Config flow for Amazon Devices integration."""
from __future__ import annotations
@@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
"""Handle a config flow for Amazon Devices."""
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -57,7 +57,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
): CountrySelector(),
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_CODE): cv.string,
vol.Required(CONF_CODE): cv.positive_int,
}
),
)

View File

@@ -1,8 +1,8 @@
"""Alexa Devices constants."""
"""Amazon Devices constants."""
import logging
_LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices"
DOMAIN = "amazon_devices"
CONF_LOGIN_DATA = "login_data"

View File

@@ -1,4 +1,4 @@
"""Support for Alexa Devices."""
"""Support for Amazon Devices."""
from datetime import timedelta
@@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
"""Base coordinator for Alexa Devices."""
"""Base coordinator for Amazon Devices."""
config_entry: AmazonConfigEntry

View File

@@ -1,7 +1,9 @@
"""Defines a base Alexa Devices entity."""
"""Defines a base Amazon Devices entity."""
from typing import cast
from aioamazondevices.api import AmazonDevice
from aioamazondevices.const import SPEAKER_GROUP_MODEL
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL, SPEAKER_GROUP_MODEL
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
@@ -12,7 +14,7 @@ from .coordinator import AmazonDevicesCoordinator
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines a base Alexa Devices entity."""
"""Defines a base Amazon Devices entity."""
_attr_has_entity_name = True
@@ -25,15 +27,17 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model_details = coordinator.api.get_model_details(self.device) or {}
model = model_details.get("model")
model_details: dict[str, str] = cast(
"dict", DEVICE_TYPE_TO_MODEL.get(self.device.device_type)
)
model = model_details["model"] if model_details else None
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model_id=self.device.device_type,
manufacturer=model_details.get("manufacturer", "Amazon"),
hw_version=model_details.get("hw_version"),
manufacturer="Amazon",
hw_version=model_details["hw_version"] if model_details else None,
sw_version=(
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
),
@@ -50,8 +54,4 @@ 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
and self.device.online
)
return super().available and self._serial_num in self.coordinator.data

View File

@@ -0,0 +1,33 @@
{
"domain": "amazon_devices",
"name": "Amazon Devices",
"codeowners": ["@chemelli74"],
"config_flow": true,
"dhcp": [
{ "macaddress": "08A6BC*" },
{ "macaddress": "10BF67*" },
{ "macaddress": "440049*" },
{ "macaddress": "443D54*" },
{ "macaddress": "48B423*" },
{ "macaddress": "4C1744*" },
{ "macaddress": "50D45C*" },
{ "macaddress": "50DCE7*" },
{ "macaddress": "68F63B*" },
{ "macaddress": "74D637*" },
{ "macaddress": "7C6166*" },
{ "macaddress": "901195*" },
{ "macaddress": "943A91*" },
{ "macaddress": "98226E*" },
{ "macaddress": "9CC8E9*" },
{ "macaddress": "A8E621*" },
{ "macaddress": "C095CF*" },
{ "macaddress": "D8BE65*" },
{ "macaddress": "EC2BEB*" }
],
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "bronze",
"requirements": ["aioamazondevices==2.0.1"]
}

View File

@@ -7,7 +7,6 @@ from dataclasses import dataclass
from typing import Any, Final
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
from aioamazondevices.const import SPEAKER_GROUP_FAMILY
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
from homeassistant.core import HomeAssistant
@@ -21,9 +20,8 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonNotifyEntityDescription(NotifyEntityDescription):
"""Alexa Devices notify entity description."""
"""Amazon Devices notify entity description."""
is_supported: Callable[[AmazonDevice], bool] = lambda _device: True
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
subkey: str
@@ -33,7 +31,6 @@ NOTIFY: Final = (
key="speak",
translation_key="speak",
subkey="AUDIO_PLAYER",
is_supported=lambda _device: _device.device_family != SPEAKER_GROUP_FAMILY,
method=lambda api, device, message: api.call_alexa_speak(device, message),
),
AmazonNotifyEntityDescription(
@@ -52,7 +49,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Alexa Devices notification entity based on a config entry."""
"""Set up Amazon Devices notification entity based on a config entry."""
coordinator = entry.runtime_data
@@ -61,7 +58,6 @@ async def async_setup_entry(
for sensor_desc in NOTIFY
for serial_num in coordinator.data
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
and sensor_desc.is_supported(coordinator.data[serial_num])
)

View File

@@ -45,9 +45,7 @@ rules:
discovery-update-info:
status: exempt
comment: Network information not relevant
discovery:
status: exempt
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo

View File

@@ -5,23 +5,23 @@
"data_description_country": "The country of your Amazon account.",
"data_description_username": "The email address of your Amazon account.",
"data_description_password": "The password of your Amazon account.",
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
"data_description_code": "The one-time password sent to your email address."
},
"config": {
"flow_title": "{username}",
"step": {
"user": {
"data": {
"country": "[%key:component::alexa_devices::common::data_country%]",
"country": "[%key:component::amazon_devices::common::data_country%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
"code": "[%key:component::amazon_devices::common::data_description_code%]"
},
"data_description": {
"country": "[%key:component::alexa_devices::common::data_description_country%]",
"username": "[%key:component::alexa_devices::common::data_description_username%]",
"password": "[%key:component::alexa_devices::common::data_description_password%]",
"code": "[%key:component::alexa_devices::common::data_description_code%]"
"country": "[%key:component::amazon_devices::common::data_description_country%]",
"username": "[%key:component::amazon_devices::common::data_description_username%]",
"password": "[%key:component::amazon_devices::common::data_description_password%]",
"code": "[%key:component::amazon_devices::common::data_description_code%]"
}
}
},

View File

@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class AmazonSwitchEntityDescription(SwitchEntityDescription):
"""Alexa Devices switch entity description."""
"""Amazon Devices switch entity description."""
is_on_fn: Callable[[AmazonDevice], bool]
subkey: str
@@ -43,7 +43,7 @@ async def async_setup_entry(
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Alexa Devices switches based on a config entry."""
"""Set up Amazon Devices switches based on a config entry."""
coordinator = entry.runtime_data

View File

@@ -62,8 +62,6 @@ async def async_setup_entry(
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
min_humidity=10,
max_humidity=50,
auto_status_key=Attribute.HUMIDIFICATION_AVAILABLE,
auto_status_value=1,
default_humidity=30,
set_humidity_fn=coordinator.client.set_humidification_setpoint,
)
@@ -79,8 +77,6 @@ async def async_setup_entry(
action_map=DEHUMIDIFIER_ACTION_MAP,
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
auto_status_key=None,
auto_status_value=None,
min_humidity=40,
max_humidity=90,
default_humidity=60,
@@ -104,8 +100,6 @@ class AprilaireHumidifierDescription(HumidifierEntityDescription):
target_humidity_key: str
min_humidity: int
max_humidity: int
auto_status_key: str | None
auto_status_value: int | None
default_humidity: int
set_humidity_fn: Callable[[int], Awaitable]
@@ -169,31 +163,14 @@ class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
def min_humidity(self) -> float:
"""Return the minimum humidity."""
if self.is_auto_humidity_mode():
return 1
return self.entity_description.min_humidity
@property
def max_humidity(self) -> float:
"""Return the maximum humidity."""
if self.is_auto_humidity_mode():
return 7
return self.entity_description.max_humidity
def is_auto_humidity_mode(self) -> bool:
"""Return whether the humidifier is in auto mode."""
if self.entity_description.auto_status_key is None:
return False
return (
self.coordinator.data.get(self.entity_description.auto_status_key)
== self.entity_description.auto_status_value
)
async def async_set_humidity(self, humidity: int) -> None:
"""Set the humidity."""

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.9.1"]
"requirements": ["pyaprilaire==0.9.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["APsystemsEZ1"],
"requirements": ["apsystems-ez1==2.7.0"]
"requirements": ["apsystems-ez1==2.6.0"]
}

View File

@@ -1,9 +1,6 @@
{
"entity": {
"sensor": {
"last_update": {
"default": "mdi:update"
},
"salt_left_side_percentage": {
"default": "mdi:basket-fill"
},

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from aioaquacell import Softener
@@ -29,7 +28,7 @@ PARALLEL_UPDATES = 1
class SoftenerSensorEntityDescription(SensorEntityDescription):
"""Describes Softener sensor entity."""
value_fn: Callable[[Softener], StateType | datetime]
value_fn: Callable[[Softener], StateType]
SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
@@ -78,12 +77,6 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
"low",
],
),
SoftenerSensorEntityDescription(
key="last_update",
translation_key="last_update",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda softener: softener.lastUpdate,
),
)
@@ -118,6 +111,6 @@ class SoftenerSensor(AquacellEntity, SensorEntity):
self.entity_description = description
@property
def native_value(self) -> StateType | datetime:
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.softener)

View File

@@ -21,9 +21,6 @@
},
"entity": {
"sensor": {
"last_update": {
"name": "Last update"
},
"salt_left_side_percentage": {
"name": "Salt left side percentage"
},

View File

@@ -89,7 +89,7 @@ class ArubaDeviceScanner(DeviceScanner):
def get_aruba_data(self) -> dict[str, dict[str, str]] | None:
"""Retrieve data from Aruba Access Point and return parsed result."""
connect = f"ssh {self.username}@{self.host}"
connect = f"ssh {self.username}@{self.host} -o HostKeyAlgorithms=ssh-rsa"
ssh: pexpect.spawn[str] = pexpect.spawn(connect, encoding="utf-8")
query = ssh.expect(
[

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.1",
"dbus-fast==2.43.0",
"habluetooth==3.49.0"
"habluetooth==3.48.2"
]
}

View File

@@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
"""Initialise a Bosch Alarm control panel entity."""
super().__init__(panel, area_id, unique_id, True, False, True)
super().__init__(panel, area_id, unique_id, False, False, True)
self._attr_unique_id = self._area_unique_id
@property

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/caldav",
"iot_class": "cloud_polling",
"loggers": ["caldav", "vobject"],
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
}

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
@@ -11,23 +10,18 @@ from homeassistant import config_entries
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
from homeassistant.helpers.json import json_dumps
_LOGGER = logging.getLogger(__name__)
@callback
def async_setup(hass: HomeAssistant) -> bool:
"""Enable the Entity Registry views."""
websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids)
websocket_api.async_register_command(hass, websocket_get_entities)
websocket_api.async_register_command(hass, websocket_get_entity)
websocket_api.async_register_command(hass, websocket_list_entities_for_display)
@@ -322,54 +316,3 @@ def websocket_remove_entity(
registry.async_remove(msg["entity_id"])
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/entity_registry/get_automatic_entity_ids",
vol.Required("entity_ids"): cv.entity_ids,
}
)
@callback
def websocket_get_automatic_entity_ids(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the automatic entity IDs for the given entity IDs.
This is used to help user reset entity IDs which have been customized by the user.
"""
registry = er.async_get(hass)
entity_ids = msg["entity_ids"]
automatic_entity_ids: dict[str, str | None] = {}
reserved_entity_ids: set[str] = set()
for entity_id in entity_ids:
if not (entry := registry.entities.get(entity_id)):
automatic_entity_ids[entity_id] = None
continue
try:
suggested = async_get_entity_suggested_object_id(hass, entity_id)
except HomeAssistantError as err:
# This is raised if the entity has no object.
_LOGGER.debug(
"Unable to get suggested object ID for %s, entity ID: %s (%s)",
entry.entity_id,
entity_id,
err,
)
automatic_entity_ids[entity_id] = None
continue
suggested_entity_id = registry.async_generate_entity_id(
entry.domain,
suggested or f"{entry.platform}_{entry.unique_id}",
current_entity_id=entity_id,
reserved_entity_ids=reserved_entity_ids,
)
automatic_entity_ids[entity_id] = suggested_entity_id
reserved_entity_ids.add(suggested_entity_id)
connection.send_message(
websocket_api.result_message(msg["id"], automatic_entity_ids)
)

View File

@@ -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.6.10"]
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
}

View File

@@ -1,3 +1 @@
"""The decora component."""
DOMAIN = "decora"

View File

@@ -21,11 +21,7 @@ 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
@@ -94,21 +90,6 @@ 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 = {}

View File

@@ -6,10 +6,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_SOURCE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -19,28 +17,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, entry.entry_id, entry.options[CONF_SOURCE]
)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SOURCE: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_SOURCE]
),
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True

View File

@@ -2,13 +2,27 @@
from __future__ import annotations
from asyncio import Semaphore
from dataclasses import dataclass
import logging
from typing import Any
from devolo_plc_api import Device
from devolo_plc_api.exceptions.device import DeviceNotFound
from devolo_plc_api.device_api import (
ConnectedStationInfo,
NeighborAPInfo,
UpdateFirmwareCheck,
WifiGuestAccessGet,
)
from devolo_plc_api.exceptions.device import (
DeviceNotFound,
DevicePasswordProtected,
DeviceUnavailable,
)
from devolo_plc_api.plcnet_api import LogicalNetwork
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_IP_ADDRESS,
CONF_PASSWORD,
@@ -16,34 +30,38 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.update_coordinator import UpdateFailed
from .const import (
CONNECTED_PLC_DEVICES,
CONNECTED_WIFI_CLIENTS,
DOMAIN,
FIRMWARE_UPDATE_INTERVAL,
LAST_RESTART,
LONG_UPDATE_INTERVAL,
NEIGHBORING_WIFI_NETWORKS,
REGULAR_FIRMWARE,
SHORT_UPDATE_INTERVAL,
SWITCH_GUEST_WIFI,
SWITCH_LEDS,
)
from .coordinator import (
DevoloDataUpdateCoordinator,
DevoloFirmwareUpdateCoordinator,
DevoloHomeNetworkConfigEntry,
DevoloHomeNetworkData,
DevoloLedSettingsGetCoordinator,
DevoloLogicalNetworkCoordinator,
DevoloUptimeGetCoordinator,
DevoloWifiConnectedStationsGetCoordinator,
DevoloWifiGuestAccessGetCoordinator,
DevoloWifiNeighborAPsGetCoordinator,
)
from .coordinator import DevoloDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData]
@dataclass
class DevoloHomeNetworkData:
"""The devolo Home Network data."""
device: Device
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
async def async_setup_entry(
hass: HomeAssistant, entry: DevoloHomeNetworkConfigEntry
@@ -51,6 +69,8 @@ async def async_setup_entry(
"""Set up devolo Home Network from a config entry."""
zeroconf_instance = await zeroconf.async_get_async_instance(hass)
async_client = get_async_client(hass)
device_registry = dr.async_get(hass)
semaphore = Semaphore(1)
try:
device = Device(
@@ -70,52 +90,177 @@ async def async_setup_entry(
entry.runtime_data = DevoloHomeNetworkData(device=device, coordinators={})
async def async_update_firmware_available() -> UpdateFirmwareCheck:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_check_firmware_available()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
async def async_update_connected_plc_devices() -> LogicalNetwork:
"""Fetch data from API endpoint."""
assert device.plcnet
update_sw_version(device_registry, device)
try:
return await device.plcnet.async_get_network_overview()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
async def async_update_guest_wifi_status() -> WifiGuestAccessGet:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_get_wifi_guest_access()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
except DevicePasswordProtected as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="password_wrong"
) from err
async def async_update_led_status() -> bool:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_get_led_setting()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
async def async_update_last_restart() -> int:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_uptime()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
except DevicePasswordProtected as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="password_wrong"
) from err
async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_get_wifi_connected_station()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]:
"""Fetch data from API endpoint."""
assert device.device
update_sw_version(device_registry, device)
try:
return await device.device.async_get_wifi_neighbor_access_points()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
async def disconnect(event: Event) -> None:
"""Disconnect from device."""
await device.async_disconnect()
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {}
if device.plcnet:
coordinators[CONNECTED_PLC_DEVICES] = DevoloLogicalNetworkCoordinator(
coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=CONNECTED_PLC_DEVICES,
semaphore=semaphore,
update_method=async_update_connected_plc_devices,
update_interval=LONG_UPDATE_INTERVAL,
)
if device.device and "led" in device.device.features:
coordinators[SWITCH_LEDS] = DevoloLedSettingsGetCoordinator(
coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=SWITCH_LEDS,
semaphore=semaphore,
update_method=async_update_led_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "restart" in device.device.features:
coordinators[LAST_RESTART] = DevoloUptimeGetCoordinator(
coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=LAST_RESTART,
semaphore=semaphore,
update_method=async_update_last_restart,
update_interval=SHORT_UPDATE_INTERVAL,
)
if device.device and "update" in device.device.features:
coordinators[REGULAR_FIRMWARE] = DevoloFirmwareUpdateCoordinator(
coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=REGULAR_FIRMWARE,
semaphore=semaphore,
update_method=async_update_firmware_available,
update_interval=FIRMWARE_UPDATE_INTERVAL,
)
if device.device and "wifi1" in device.device.features:
coordinators[CONNECTED_WIFI_CLIENTS] = (
DevoloWifiConnectedStationsGetCoordinator(
hass,
_LOGGER,
config_entry=entry,
)
)
coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloWifiNeighborAPsGetCoordinator(
coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=CONNECTED_WIFI_CLIENTS,
semaphore=semaphore,
update_method=async_update_wifi_connected_station,
update_interval=SHORT_UPDATE_INTERVAL,
)
coordinators[SWITCH_GUEST_WIFI] = DevoloWifiGuestAccessGetCoordinator(
coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=NEIGHBORING_WIFI_NETWORKS,
semaphore=semaphore,
update_method=async_update_wifi_neighbor_access_points,
update_interval=LONG_UPDATE_INTERVAL,
)
coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=SWITCH_GUEST_WIFI,
semaphore=semaphore,
update_method=async_update_guest_wifi_status,
update_interval=SHORT_UPDATE_INTERVAL,
)
for coordinator in coordinators.values():
@@ -158,3 +303,16 @@ def platforms(device: Device) -> set[Platform]:
if device.device and "update" in device.device.features:
supported_platforms.add(Platform.UPDATE)
return supported_platforms
@callback
def update_sw_version(device_registry: dr.DeviceRegistry, device: Device) -> None:
"""Update device registry with new firmware version."""
if (
device_entry := device_registry.async_get_device(
identifiers={(DOMAIN, str(device.serial_number))}
)
) and device_entry.sw_version != device.firmware_version:
device_registry.async_update_device(
device_id=device_entry.id, sw_version=device.firmware_version
)

View File

@@ -16,8 +16,9 @@ from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 0

View File

@@ -18,8 +18,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS
from .coordinator import DevoloHomeNetworkConfigEntry
from .entity import DevoloEntity
PARALLEL_UPDATES = 0

View File

@@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE
from .coordinator import DevoloHomeNetworkConfigEntry
_LOGGER = logging.getLogger(__name__)

View File

@@ -1,44 +1,13 @@
"""Base coordinator."""
from asyncio import Semaphore
from dataclasses import dataclass
from collections.abc import Awaitable, Callable
from datetime import timedelta
from logging import Logger
from typing import Any
from devolo_plc_api import Device
from devolo_plc_api.device_api import (
ConnectedStationInfo,
NeighborAPInfo,
UpdateFirmwareCheck,
WifiGuestAccessGet,
)
from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable
from devolo_plc_api.plcnet_api import LogicalNetwork
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
CONNECTED_PLC_DEVICES,
CONNECTED_WIFI_CLIENTS,
DOMAIN,
FIRMWARE_UPDATE_INTERVAL,
LAST_RESTART,
LONG_UPDATE_INTERVAL,
NEIGHBORING_WIFI_NETWORKS,
REGULAR_FIRMWARE,
SHORT_UPDATE_INTERVAL,
SWITCH_GUEST_WIFI,
SWITCH_LEDS,
)
SEMAPHORE = Semaphore(1)
type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData]
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
@@ -49,256 +18,24 @@ class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
hass: HomeAssistant,
logger: Logger,
*,
config_entry: DevoloHomeNetworkConfigEntry,
config_entry: ConfigEntry,
name: str,
update_interval: timedelta | None = None,
semaphore: Semaphore,
update_interval: timedelta,
update_method: Callable[[], Awaitable[_DataT]],
) -> None:
"""Initialize global data updater."""
self.device = config_entry.runtime_data.device
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
update_method=update_method,
)
self._semaphore = semaphore
async def _async_update_data(self) -> _DataT:
"""Fetch the latest data from the source."""
self.update_sw_version()
async with SEMAPHORE:
try:
return await super()._async_update_data()
except DeviceUnavailable as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
) from err
except DevicePasswordProtected as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="password_wrong"
) from err
@callback
def update_sw_version(self) -> None:
"""Update device registry with new firmware version, if it changed at runtime."""
device_registry = dr.async_get(self.hass)
if (
device_entry := device_registry.async_get_device(
identifiers={(DOMAIN, self.device.serial_number)}
)
) and device_entry.sw_version != self.device.firmware_version:
device_registry.async_update_device(
device_id=device_entry.id, sw_version=self.device.firmware_version
)
class DevoloFirmwareUpdateCoordinator(DevoloDataUpdateCoordinator[UpdateFirmwareCheck]):
"""Class to manage fetching data from the UpdateFirmwareCheck endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = REGULAR_FIRMWARE,
update_interval: timedelta | None = FIRMWARE_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_firmware_available
async def async_update_firmware_available(self) -> UpdateFirmwareCheck:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_check_firmware_available()
class DevoloLedSettingsGetCoordinator(DevoloDataUpdateCoordinator[bool]):
"""Class to manage fetching data from the LedSettingsGet endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = SWITCH_LEDS,
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_led_status
async def async_update_led_status(self) -> bool:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_get_led_setting()
class DevoloLogicalNetworkCoordinator(DevoloDataUpdateCoordinator[LogicalNetwork]):
"""Class to manage fetching data from the GetNetworkOverview endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = CONNECTED_PLC_DEVICES,
update_interval: timedelta | None = LONG_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_connected_plc_devices
async def async_update_connected_plc_devices(self) -> LogicalNetwork:
"""Fetch data from API endpoint."""
assert self.device.plcnet
return await self.device.plcnet.async_get_network_overview()
class DevoloUptimeGetCoordinator(DevoloDataUpdateCoordinator[int]):
"""Class to manage fetching data from the UptimeGet endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = LAST_RESTART,
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_last_restart
async def async_update_last_restart(self) -> int:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_uptime()
class DevoloWifiConnectedStationsGetCoordinator(
DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]
):
"""Class to manage fetching data from the WifiGuestAccessGet endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = CONNECTED_WIFI_CLIENTS,
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_get_wifi_connected_station
async def async_get_wifi_connected_station(self) -> list[ConnectedStationInfo]:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_get_wifi_connected_station()
class DevoloWifiGuestAccessGetCoordinator(
DevoloDataUpdateCoordinator[WifiGuestAccessGet]
):
"""Class to manage fetching data from the WifiGuestAccessGet endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = SWITCH_GUEST_WIFI,
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_guest_wifi_status
async def async_update_guest_wifi_status(self) -> WifiGuestAccessGet:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_get_wifi_guest_access()
class DevoloWifiNeighborAPsGetCoordinator(
DevoloDataUpdateCoordinator[list[NeighborAPInfo]]
):
"""Class to manage fetching data from the WifiNeighborAPsGet endpoint."""
def __init__(
self,
hass: HomeAssistant,
logger: Logger,
*,
config_entry: ConfigEntry,
name: str = NEIGHBORING_WIFI_NETWORKS,
update_interval: timedelta | None = LONG_UPDATE_INTERVAL,
) -> None:
"""Initialize global data updater."""
super().__init__(
hass,
logger,
config_entry=config_entry,
name=name,
update_interval=update_interval,
)
self.update_method = self.async_update_wifi_neighbor_access_points
async def async_update_wifi_neighbor_access_points(self) -> list[NeighborAPInfo]:
"""Fetch data from API endpoint."""
assert self.device.device
return await self.device.device.async_get_wifi_neighbor_access_points()
@dataclass
class DevoloHomeNetworkData:
"""The devolo Home Network data."""
device: Device
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
async with self._semaphore:
return await super()._async_update_data()

View File

@@ -15,8 +15,9 @@ from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DevoloHomeNetworkConfigEntry
from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
PARALLEL_UPDATES = 0

View File

@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD
from homeassistant.core import HomeAssistant
from .coordinator import DevoloHomeNetworkConfigEntry
from . import DevoloHomeNetworkConfigEntry
TO_REDACT = {CONF_PASSWORD}

View File

@@ -15,8 +15,9 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
type _DataType = (
LogicalNetwork

View File

@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from . import DevoloHomeNetworkConfigEntry
from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 0

View File

@@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.dt import utcnow
from . import DevoloHomeNetworkConfigEntry
from .const import (
CONNECTED_PLC_DEVICES,
CONNECTED_WIFI_CLIENTS,
@@ -30,7 +31,7 @@ from .const import (
PLC_RX_RATE,
PLC_TX_RATE,
)
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 0

View File

@@ -16,8 +16,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 0

View File

@@ -21,8 +21,9 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DevoloHomeNetworkConfigEntry
from .const import DOMAIN, REGULAR_FIRMWARE
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
from .coordinator import DevoloDataUpdateCoordinator
from .entity import DevoloCoordinatorEntity
PARALLEL_UPDATES = 0

View File

@@ -1,3 +1 @@
"""The dlib_face_detect component."""
DOMAIN = "dlib_face_detect"

View File

@@ -11,17 +11,10 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.core import 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
@@ -32,20 +25,6 @@ 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))

View File

@@ -1,4 +1 @@
"""The dlib_face_identify component."""
CONF_FACES = "faces"
DOMAIN = "dlib_face_identify"

View File

@@ -15,20 +15,14 @@ from homeassistant.components.image_processing import (
ImageProcessingFaceEntity,
)
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
from homeassistant.core import (
DOMAIN as HOMEASSISTANT_DOMAIN,
HomeAssistant,
split_entity_id,
)
from homeassistant.core import 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(
{
@@ -45,21 +39,6 @@ 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]

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
import asyncio
import contextlib
from typing import Any, Literal
from typing import Any
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: Literal["A", "AAAA"], port: int = 53
hostname: str, resolver: str, qtype: str, port: int = 53
) -> bool:
"""Return if able to resolve hostname."""
result: bool = False
result = False
with contextlib.suppress(DNSError):
_resolver = aiodns.DNSResolver(
nameservers=[resolver], udp_port=port, tcp_port=port
result = bool(
await aiodns.DNSResolver( # type: ignore[call-overload]
nameservers=[resolver], udp_port=port, tcp_port=port
).query(hostname, qtype)
)
result = bool(await _resolver.query(hostname, qtype))
return result
result: dict[str, bool] = {}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.5.0"]
"requirements": ["aiodns==3.4.0"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.3.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
}

View File

@@ -1,6 +1 @@
"""The eddystone_temperature component."""
DOMAIN = "eddystone_temperature"
CONF_BEACONS = "beacons"
CONF_INSTANCE = "instance"
CONF_NAMESPACE = "namespace"

View File

@@ -23,18 +23,17 @@ from homeassistant.const import (
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
from homeassistant.core import 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(
{
@@ -59,21 +58,6 @@ 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]

View File

@@ -1,19 +0,0 @@
"""Diagnostics for the EHEIM Digital integration."""
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .coordinator import EheimDigitalConfigEntry
TO_REDACT = {"emailAddr", "usrName"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: EheimDigitalConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return async_redact_data(
{"entry": entry.as_dict(), "data": entry.runtime_data.data}, TO_REDACT
)

View File

@@ -94,6 +94,8 @@ class EheimDigitalClassicLEDControlLight(
await self._device.set_light_mode(EFFECT_TO_LIGHT_MODE[kwargs[ATTR_EFFECT]])
return
if ATTR_BRIGHTNESS in kwargs:
if self._device.light_mode == LightMode.DAYCL_MODE:
await self._device.set_light_mode(LightMode.MAN_MODE)
await self._device.turn_on(
int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])),
self._channel,
@@ -102,6 +104,8 @@ class EheimDigitalClassicLEDControlLight(
@exception_handler
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the light."""
if self._device.light_mode == LightMode.DAYCL_MODE:
await self._device.set_light_mode(LightMode.MAN_MODE)
await self._device.turn_off(self._channel)
def _async_update_attrs(self) -> None:

View File

@@ -43,7 +43,7 @@ rules:
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery-update-info: done
discovery: done
docs-data-update: todo

View File

@@ -180,15 +180,9 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
)
return
device_registry.async_get_or_create(
config_entry_id=self.config_entry.entry_id,
identifiers={
(
DOMAIN,
self.envoy_serial_number,
)
},
connections={connection},
device_registry.async_update_device(
device_id=envoy_device.id,
new_connections={connection},
)
_LOGGER.debug("added connection: %s to %s", connection, self.name)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.11.2"]
"requirements": ["env-canada==0.10.2"]
}

View File

@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
}

View File

@@ -226,7 +226,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
_static_info: _InfoT
_state: _StateT
_has_state: bool
unique_id: str
def __init__(
self,

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
from functools import partial
import logging
import re
from typing import TYPE_CHECKING, Any, NamedTuple
from aioesphomeapi import (
@@ -22,7 +23,6 @@ from aioesphomeapi import (
RequiresEncryptionAPIError,
UserService,
UserServiceArgType,
parse_log_message,
)
from awesomeversion import AwesomeVersion
import voluptuous as vol
@@ -110,6 +110,11 @@ LOGGER_TO_LOG_LEVEL = {
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
}
# 7-bit and 8-bit C1 ANSI sequences
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
ANSI_ESCAPE_78BIT = re.compile(
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
)
@callback
@@ -382,15 +387,13 @@ class ESPHomeManager:
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
"""Handle a log message from the API."""
for line in parse_log_message(
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
):
_LOGGER.log(
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
"%s: %s",
self.entry.title,
line,
)
log: bytes = msg.message
_LOGGER.log(
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
"%s: %s",
self.entry.title,
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
)
@callback
def _async_get_equivalent_log_level(self) -> LogLevel:

View File

@@ -17,9 +17,9 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==32.2.1",
"aioesphomeapi==31.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.16.0"
"bleak-esphome==2.15.1"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
if self._static_info.supports_pause:
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
self._attr_supported_features = flags
self._entry_data.media_player_formats[self.unique_id] = cast(
self._entry_data.media_player_formats[static_info.unique_id] = cast(
MediaPlayerInfo, static_info
).supported_formats
@@ -114,8 +114,9 @@ class EsphomeMediaPlayer(
media_id = async_process_play_media_url(self.hass, media_id)
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
supported_formats: list[MediaPlayerSupportedFormat] | None = (
self._entry_data.media_player_formats.get(self.unique_id)
self._entry_data.media_player_formats.get(self._static_info.unique_id)
)
if (
@@ -138,7 +139,7 @@ class EsphomeMediaPlayer(
async def async_will_remove_from_hass(self) -> None:
"""Handle entity being removed."""
await super().async_will_remove_from_hass()
self._entry_data.media_player_formats.pop(self.unique_id, None)
self._entry_data.media_player_formats.pop(self.entity_id, None)
def _get_proxy_url(
self,

View File

@@ -105,18 +105,9 @@ DATA_SCHEMA_SETUP = vol.Schema(
)
BASE_OPTIONS_SCHEMA = {
vol.Optional(CONF_ENTITY_ID): EntitySelector(EntitySelectorConfig(read_only=True)),
vol.Optional(CONF_FILTER_NAME): SelectSelector(
SelectSelectorConfig(
options=FILTERS,
mode=SelectSelectorMode.DROPDOWN,
translation_key=CONF_FILTER_NAME,
read_only=True,
)
),
vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): NumberSelector(
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
),
)
}
OUTLIER_SCHEMA = vol.Schema(

View File

@@ -23,16 +23,12 @@
"data": {
"window_size": "Window size",
"precision": "Precision",
"radius": "Radius",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"radius": "Radius"
},
"data_description": {
"window_size": "Size of the window of previous states.",
"precision": "Defines the number of decimal places of the calculated sensor value.",
"radius": "Band radius from median of previous states.",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"radius": "Band radius from median of previous states."
}
},
"lowpass": {
@@ -40,16 +36,12 @@
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"time_constant": "Time constant",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"time_constant": "Time constant"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"time_constant": "Loosely relates to the amount of time it takes for a state to influence the output.",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"time_constant": "Loosely relates to the amount of time it takes for a state to influence the output."
}
},
"range": {
@@ -57,16 +49,12 @@
"data": {
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"lower_bound": "Lower bound",
"upper_bound": "Upper bound",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"upper_bound": "Upper bound"
},
"data_description": {
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"lower_bound": "Lower bound for filter range.",
"upper_bound": "Upper bound for filter range.",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"upper_bound": "Upper bound for filter range."
}
},
"time_simple_moving_average": {
@@ -74,46 +62,34 @@
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"type": "Type",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"type": "Type"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"type": "Defines the type of Simple Moving Average.",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"type": "Defines the type of Simple Moving Average."
}
},
"throttle": {
"description": "[%key:component::filter::config::step::outlier::description%]",
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
}
},
"time_throttle": {
"description": "[%key:component::filter::config::step::outlier::description%]",
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
}
}
}
@@ -128,16 +104,12 @@
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"radius": "[%key:component::filter::config::step::outlier::data::radius%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"radius": "[%key:component::filter::config::step::outlier::data::radius%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"radius": "[%key:component::filter::config::step::outlier::data_description::radius%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"radius": "[%key:component::filter::config::step::outlier::data_description::radius%]"
}
},
"lowpass": {
@@ -145,16 +117,12 @@
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]"
}
},
"range": {
@@ -162,16 +130,12 @@
"data": {
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"lower_bound": "[%key:component::filter::config::step::range::data::lower_bound%]",
"upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]"
},
"data_description": {
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"lower_bound": "[%key:component::filter::config::step::range::data_description::lower_bound%]",
"upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]"
}
},
"time_simple_moving_average": {
@@ -179,46 +143,34 @@
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]"
}
},
"throttle": {
"description": "[%key:component::filter::config::step::outlier::description%]",
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
}
},
"time_throttle": {
"description": "[%key:component::filter::config::step::outlier::description%]",
"data": {
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
},
"data_description": {
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
}
}
}

View File

@@ -9,7 +9,6 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
@@ -29,7 +28,6 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
key="rate_down",
name="Freebox download speed",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:download-network",
),
@@ -37,7 +35,6 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
key="rate_up",
name="Freebox upload speed",
device_class=SensorDeviceClass.DATA_RATE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
icon="mdi:upload-network",
),
@@ -84,7 +81,6 @@ 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

View File

@@ -12,5 +12,5 @@
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",
"requirements": ["PyFronius==0.8.0"]
"requirements": ["PyFronius==0.7.7"]
}

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250531.3"]
"requirements": ["home-assistant-frontend==20250516.0"]
}

View File

@@ -5,18 +5,11 @@ import voluptuous as vol
from homeassistant.components.humidifier import HumidifierDeviceClass
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import (
config_validation as cv,
discovery,
entity_registry as er,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.typing import ConfigType
DOMAIN = "generic_hygrostat"
@@ -95,54 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.options[CONF_HUMIDIFIER],
)
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the humidifer,
# but not the humidity sensor because the generic_hygrostat adds itself to the
# humidifier's device.
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HUMIDIFIER]
),
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
source_entity_removed=source_entity_removed,
)
)
async def async_sensor_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] != "update":
return
if "entity_id" not in data["changes"]:
return
# Entity_id changed, update the config entry
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SENSOR: data["entity_id"]},
)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, entry.options[CONF_SENSOR], async_sensor_updated
)
)
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True

View File

@@ -1,16 +1,12 @@
"""The generic_thermostat component."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
from .const import CONF_HEATER, PLATFORMS
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -21,55 +17,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.entry_id,
entry.options[CONF_HEATER],
)
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_HEATER: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we need to clean the device links.
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
entry.async_on_unload(
# We use async_handle_source_entity_changes to track changes to the heater, but
# not the temperature sensor because the generic_hygrostat adds itself to the
# heater's device.
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_HEATER]
),
source_entity_id_or_uuid=entry.options[CONF_HEATER],
source_entity_removed=source_entity_removed,
)
)
async def async_sensor_updated(
event: Event[er.EventEntityRegistryUpdatedData],
) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] != "update":
return
if "entity_id" not in data["changes"]:
return
# Entity_id changed, update the config entry
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_SENSOR: data["entity_id"]},
)
entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, entry.options[CONF_SENSOR], async_sensor_updated
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
return True

View File

@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["go2rtc-client==0.2.1"],
"requirements": ["go2rtc-client==0.1.3b0"],
"single_config_entry": true
}

View File

@@ -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==10.0.0"]
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
}

View File

@@ -12,7 +12,6 @@ import aiohttp
from aiohttp import web
from gassist_text import TextAssistant
from google.oauth2.credentials import Credentials
from grpc import RpcError
from homeassistant.components.http import HomeAssistantView
from homeassistant.components.media_player import (
@@ -26,7 +25,6 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
from homeassistant.helpers.event import async_call_later
@@ -85,17 +83,7 @@ async def async_send_text_commands(
) as assistant:
command_response_list = []
for command in commands:
try:
resp = await hass.async_add_executor_job(assistant.assist, command)
except RpcError as err:
_LOGGER.error(
"Failed to send command '%s' to Google Assistant: %s",
command,
err,
)
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="grpc_error"
) from err
resp = await hass.async_add_executor_job(assistant.assist, command)
text_response = resp[0]
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
audio_response = resp[2]

View File

@@ -57,10 +57,5 @@
}
}
}
},
"exceptions": {
"grpc_error": {
"message": "Failed to communicate with Google Assistant"
}
}
}

View File

@@ -50,12 +50,7 @@ from .const import (
UNITS_IMPERIAL,
UNITS_METRIC,
)
from .helpers import (
InvalidApiKeyException,
PermissionDeniedException,
UnknownException,
validate_config_entry,
)
from .helpers import InvalidApiKeyException, UnknownException, validate_config_entry
RECONFIGURE_SCHEMA = vol.Schema(
{
@@ -193,8 +188,6 @@ 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:

View File

@@ -7,7 +7,6 @@ from google.api_core.exceptions import (
Forbidden,
GatewayTimeout,
GoogleAPIError,
PermissionDenied,
Unauthorized,
)
from google.maps.routing_v2 import (
@@ -20,18 +19,10 @@ 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__)
@@ -46,7 +37,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.Invalid):
except (AttributeError, vol.ExactSequenceInvalid):
return Waypoint(address=location)
return Waypoint(
location=Location(
@@ -76,9 +67,6 @@ 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
@@ -96,30 +84,3 @@ 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}")

View File

@@ -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, PermissionDenied
from google.api_core.exceptions import GoogleAPIError
from google.maps.routing_v2 import (
ComputeRoutesRequest,
Route,
@@ -58,11 +58,7 @@ from .const import (
TRAVEL_MODES_TO_GOOGLE_SDK_ENUM,
UNITS_TO_GOOGLE_SDK_ENUM,
)
from .helpers import (
convert_to_waypoint,
create_routes_api_disabled_issue,
delete_routes_api_disabled_issue,
)
from .helpers import convert_to_waypoint
_LOGGER = logging.getLogger(__name__)
@@ -275,14 +271,8 @@ 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

View File

@@ -21,7 +21,6 @@
}
},
"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%]"
@@ -101,11 +100,5 @@
"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."
}
}
}

View File

@@ -1,3 +1 @@
"""The gstreamer component."""
DOMAIN = "gstreamer"

View File

@@ -19,18 +19,16 @@ from homeassistant.components.media_player import (
async_process_play_media_url,
)
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core import 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}
@@ -50,20 +48,6 @@ 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)

View File

@@ -9,10 +9,8 @@ from functools import partial
import logging
import os
import re
import struct
from typing import Any, NamedTuple
import aiofiles
from aiohasupervisor import SupervisorError
import voluptuous as vol
@@ -39,7 +37,6 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery_flow,
issue_registry as ir,
)
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.deprecation import (
@@ -54,7 +51,6 @@ from homeassistant.helpers.hassio import (
get_supervisor_ip as _get_supervisor_ip,
is_hassio as _is_hassio,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service_info.hassio import (
HassioServiceInfo as _HassioServiceInfo,
)
@@ -113,7 +109,7 @@ from .coordinator import (
get_core_info, # noqa: F401
get_core_stats, # noqa: F401
get_host_info, # noqa: F401
get_info,
get_info, # noqa: F401
get_issues_info, # noqa: F401
get_os_info,
get_supervisor_info, # noqa: F401
@@ -172,11 +168,6 @@ SERVICE_RESTORE_PARTIAL = "restore_partial"
VALID_ADDON_SLUG = vol.Match(re.compile(r"^[-_.A-Za-z0-9]+$"))
DEPRECATION_URL = (
"https://www.home-assistant.io/blog/2025/05/22/"
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
)
def valid_addon(value: Any) -> str:
"""Validate value is a valid addon slug."""
@@ -234,17 +225,6 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
)
def _is_32_bit() -> bool:
size = struct.calcsize("P")
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = await arch_file.read()
return {"x86": "i386"}.get(raw_arch, raw_arch)
class APIEndpointSettings(NamedTuple):
"""Settings for API endpoint."""
@@ -566,62 +546,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data[ADDONS_COORDINATOR] = coordinator
arch = await _get_arch()
def deprecated_setup_issue() -> None:
os_info = get_os_info(hass)
info = get_info(hass)
if os_info is None or info is None:
return
is_haos = info.get("hassos") is not None
board = os_info.get("board")
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
unsupported_os_on_board = board in {"rpi3", "rpi4"}
if is_haos and (unsupported_board or unsupported_os_on_board):
issue_id = "deprecated_os_"
if unsupported_os_on_board:
issue_id += "aarch64"
elif unsupported_board:
issue_id += "armv7"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_guide": "https://www.home-assistant.io/installation/",
},
)
bit32 = _is_32_bit()
deprecated_architecture = bit32 and not (
unsupported_board or unsupported_os_on_board
)
if not is_haos or deprecated_architecture:
issue_id = "deprecated"
if not is_haos:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
"homeassistant",
issue_id,
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": "OS" if is_haos else "Supervised",
"arch": arch,
},
)
listener()
listener = coordinator.async_add_listener(deprecated_setup_issue)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -1,3 +1 @@
"""The hddtemp component."""
DOMAIN = "hddtemp"

View File

@@ -22,14 +22,11 @@ from homeassistant.const import (
CONF_PORT,
UnitOfTemperature,
)
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
from homeassistant.core import 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"
@@ -59,21 +56,6 @@ 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)

View File

@@ -5,13 +5,26 @@ from __future__ import annotations
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started
from homeassistant.util import dt as dt_util
from .const import TRAVEL_MODE_PUBLIC
from .const import (
CONF_ARRIVAL_TIME,
CONF_DEPARTURE_TIME,
CONF_DESTINATION_ENTITY_ID,
CONF_DESTINATION_LATITUDE,
CONF_DESTINATION_LONGITUDE,
CONF_ORIGIN_ENTITY_ID,
CONF_ORIGIN_LATITUDE,
CONF_ORIGIN_LONGITUDE,
CONF_ROUTE_MODE,
TRAVEL_MODE_PUBLIC,
)
from .coordinator import (
HereConfigEntry,
HERERoutingDataUpdateCoordinator,
HERETransitDataUpdateCoordinator,
)
from .model import HERETravelTimeConfig
PLATFORMS = [Platform.SENSOR]
@@ -20,13 +33,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry)
"""Set up HERE Travel Time from a config entry."""
api_key = config_entry.data[CONF_API_KEY]
arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, ""))
departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, ""))
here_travel_time_config = HERETravelTimeConfig(
destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
travel_mode=config_entry.data[CONF_MODE],
route_mode=config_entry.options[CONF_ROUTE_MODE],
arrival=arrival,
departure=departure,
)
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
cls = HERETransitDataUpdateCoordinator
else:
cls = HERERoutingDataUpdateCoordinator
data_coordinator = cls(hass, config_entry, api_key)
data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config)
config_entry.runtime_data = data_coordinator
async def _async_update_at_start(_: HomeAssistant) -> None:

View File

@@ -26,7 +26,7 @@ from here_transit import (
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODE, UnitOfLength
from homeassistant.const import UnitOfLength
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.location import find_coordinates
@@ -34,21 +34,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import DistanceConverter
from .const import (
CONF_ARRIVAL_TIME,
CONF_DEPARTURE_TIME,
CONF_DESTINATION_ENTITY_ID,
CONF_DESTINATION_LATITUDE,
CONF_DESTINATION_LONGITUDE,
CONF_ORIGIN_ENTITY_ID,
CONF_ORIGIN_LATITUDE,
CONF_ORIGIN_LONGITUDE,
CONF_ROUTE_MODE,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
ROUTE_MODE_FASTEST,
)
from .model import HERETravelTimeAPIParams, HERETravelTimeData
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
from .model import HERETravelTimeConfig, HERETravelTimeData
BACKOFF_MULTIPLIER = 1.1
@@ -60,7 +47,7 @@ type HereConfigEntry = ConfigEntry[
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
"""HERETravelTime DataUpdateCoordinator for the routing API."""
"""here_routing DataUpdateCoordinator."""
config_entry: HereConfigEntry
@@ -69,6 +56,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
hass: HomeAssistant,
config_entry: HereConfigEntry,
api_key: str,
config: HERETravelTimeConfig,
) -> None:
"""Initialize."""
super().__init__(
@@ -79,34 +67,41 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self._api = HERERoutingApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData:
"""Get the latest data from the HERE Routing API."""
params = prepare_parameters(self.hass, self.config_entry)
origin, destination, arrival, departure = prepare_parameters(
self.hass, self.config
)
route_mode = (
RoutingMode.FAST
if self.config.route_mode == ROUTE_MODE_FASTEST
else RoutingMode.SHORT
)
_LOGGER.debug(
(
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
" mode: %s, arrival: %s, departure: %s"
),
params.origin,
params.destination,
params.route_mode,
TransportMode(params.travel_mode),
params.arrival,
params.departure,
origin,
destination,
route_mode,
TransportMode(self.config.travel_mode),
arrival,
departure,
)
try:
response = await self._api.route(
transport_mode=TransportMode(params.travel_mode),
origin=here_routing.Place(params.origin[0], params.origin[1]),
destination=here_routing.Place(
params.destination[0], params.destination[1]
),
routing_mode=params.route_mode,
arrival_time=params.arrival,
departure_time=params.departure,
transport_mode=TransportMode(self.config.travel_mode),
origin=here_routing.Place(origin[0], origin[1]),
destination=here_routing.Place(destination[0], destination[1]),
routing_mode=route_mode,
arrival_time=arrival,
departure_time=departure,
return_values=[Return.POLYINE, Return.SUMMARY],
spans=[Spans.NAMES],
)
@@ -180,7 +175,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
class HERETransitDataUpdateCoordinator(
DataUpdateCoordinator[HERETravelTimeData | None]
):
"""HERETravelTime DataUpdateCoordinator for the transit API."""
"""HERETravelTime DataUpdateCoordinator."""
config_entry: HereConfigEntry
@@ -189,6 +184,7 @@ class HERETransitDataUpdateCoordinator(
hass: HomeAssistant,
config_entry: HereConfigEntry,
api_key: str,
config: HERETravelTimeConfig,
) -> None:
"""Initialize."""
super().__init__(
@@ -199,31 +195,32 @@ class HERETransitDataUpdateCoordinator(
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
)
self._api = HERETransitApi(api_key)
self.config = config
async def _async_update_data(self) -> HERETravelTimeData | None:
"""Get the latest data from the HERE Routing API."""
params = prepare_parameters(self.hass, self.config_entry)
origin, destination, arrival, departure = prepare_parameters(
self.hass, self.config
)
_LOGGER.debug(
(
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
" departure: %s"
),
params.origin,
params.destination,
params.arrival,
params.departure,
origin,
destination,
arrival,
departure,
)
try:
response = await self._api.route(
origin=here_transit.Place(
latitude=params.origin[0], longitude=params.origin[1]
),
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
destination=here_transit.Place(
latitude=params.destination[0], longitude=params.destination[1]
latitude=destination[0], longitude=destination[1]
),
arrival_time=params.arrival,
departure_time=params.departure,
arrival_time=arrival,
departure_time=departure,
return_values=[
here_transit.Return.POLYLINE,
here_transit.Return.TRAVEL_SUMMARY,
@@ -288,8 +285,8 @@ class HERETransitDataUpdateCoordinator(
def prepare_parameters(
hass: HomeAssistant,
config_entry: HereConfigEntry,
) -> HERETravelTimeAPIParams:
config: HERETravelTimeConfig,
) -> tuple[list[str], list[str], str | None, str | None]:
"""Prepare parameters for the HERE api."""
def _from_entity_id(entity_id: str) -> list[str]:
@@ -308,55 +305,32 @@ def prepare_parameters(
return formatted_coordinates
# Destination
if (
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
) is not None:
destination = _from_entity_id(str(destination_entity_id))
if config.destination_entity_id is not None:
destination = _from_entity_id(config.destination_entity_id)
else:
destination = [
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
str(config.destination_latitude),
str(config.destination_longitude),
]
# Origin
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
origin = _from_entity_id(str(origin_entity_id))
if config.origin_entity_id is not None:
origin = _from_entity_id(config.origin_entity_id)
else:
origin = [
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
str(config.origin_latitude),
str(config.origin_longitude),
]
# Arrival/Departure
arrival: datetime | None = None
if (
conf_arrival := dt_util.parse_time(
config_entry.options.get(CONF_ARRIVAL_TIME, "")
)
) is not None:
arrival = next_datetime(conf_arrival)
departure: datetime | None = None
if (
conf_departure := dt_util.parse_time(
config_entry.options.get(CONF_DEPARTURE_TIME, "")
)
) is not None:
departure = next_datetime(conf_departure)
arrival: str | None = None
departure: str | None = None
if config.arrival is not None:
arrival = next_datetime(config.arrival).isoformat()
if config.departure is not None:
departure = next_datetime(config.departure).isoformat()
route_mode = (
RoutingMode.FAST
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
else RoutingMode.SHORT
)
return HERETravelTimeAPIParams(
destination=destination,
origin=origin,
travel_mode=config_entry.data[CONF_MODE],
route_mode=route_mode,
arrival=arrival,
departure=departure,
)
return (origin, destination, arrival, departure)
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from datetime import time
from typing import TypedDict
@@ -21,12 +21,16 @@ class HERETravelTimeData(TypedDict):
@dataclass
class HERETravelTimeAPIParams:
"""Configuration for polling the HERE API."""
class HERETravelTimeConfig:
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
destination: list[str]
origin: list[str]
destination_latitude: float | None
destination_longitude: float | None
destination_entity_id: str | None
origin_latitude: float | None
origin_longitude: float | None
origin_entity_id: str | None
travel_mode: str
route_mode: str
arrival: datetime | None
departure: datetime | None
arrival: time | None
departure: time | None

View File

@@ -8,10 +8,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_entity_id_to_device_id,
async_remove_stale_devices_links_keep_entity_device,
)
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
from homeassistant.helpers.template import Template
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
@@ -53,30 +51,6 @@ async def async_setup_entry(
entry.options[CONF_ENTITY_ID],
)
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
hass.config_entries.async_update_entry(
entry,
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
)
async def source_entity_removed() -> None:
# The source entity has been removed, we remove the config entry because
# history_stats does not allow replacing the input entity.
await hass.config_entries.async_remove(entry.entry_id)
entry.async_on_unload(
async_handle_source_entity_changes(
hass,
helper_config_entry_id=entry.entry_id,
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
source_device_id=async_entity_id_to_device_id(
hass, entry.options[CONF_ENTITY_ID]
),
source_entity_id_or_uuid=entry.options[CONF_ENTITY_ID],
source_entity_removed=source_entity_removed,
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))

View File

@@ -107,7 +107,7 @@ OPTIONS_FLOW = {
}
class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
class StatisticsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
"""Handle a config flow for History stats."""
config_flow = CONFIG_FLOW

View File

@@ -25,12 +25,17 @@ def _get_obj_holidays_and_language(
selected_categories: list[str] | None,
) -> tuple[HolidayBase, str]:
"""Get the object for the requested country and year."""
if selected_categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *selected_categories]
obj_holidays = country_holidays(
country,
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=language,
categories=selected_categories,
categories=categories,
)
if language == "en":
for lang in obj_holidays.supported_languages:
@@ -40,7 +45,7 @@ def _get_obj_holidays_and_language(
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=lang,
categories=selected_categories,
categories=categories,
)
language = lang
break
@@ -54,7 +59,7 @@ def _get_obj_holidays_and_language(
subdiv=province,
years={dt_util.now().year, dt_util.now().year + 1},
language=default_language,
categories=selected_categories,
categories=categories,
)
language = default_language
@@ -72,11 +77,6 @@ async def async_setup_entry(
categories: list[str] | None = config_entry.options.get(CONF_CATEGORIES)
language = hass.config.language
if categories is None:
categories = [PUBLIC]
else:
categories = [PUBLIC, *categories]
obj_holidays, language = await hass.async_add_executor_job(
_get_obj_holidays_and_language, country, province, language, categories
)

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.74", "babel==2.15.0"]
"requirements": ["holidays==0.73", "babel==2.15.0"]
}

View File

@@ -12,13 +12,3 @@ async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationSe
authorize_url=OAUTH2_AUTHORIZE,
token_url=OAUTH2_TOKEN,
)
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
"""Return description placeholders for the credentials dialog."""
return {
"developer_dashboard_url": "https://developer.home-connect.com/",
"applications_url": "https://developer.home-connect.com/applications",
"register_application_url": "https://developer.home-connect.com/application/add",
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
}

View File

@@ -10,17 +10,17 @@
"macaddress": "C8D778*"
},
{
"hostname": "(balay|bosch|neff|siemens)-*",
"hostname": "(bosch|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "(siemens|neff)-*",
"hostname": "siemens-*",
"macaddress": "38B4D3*"
}
],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"requirements": ["aiohomeconnect==0.17.1"],
"requirements": ["aiohomeconnect==0.17.0"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -1,7 +1,4 @@
{
"application_credentials": {
"description": "Login to Home Connect requires a client ID and secret. To acquire them, please follow the following steps.\n\n1. Visit the [Home Connect Developer Program website]({developer_dashboard_url}) and sign up for a development account.\n1. Enter the email of your login for the original Home Connect app under **Default Home Connect User Account for Testing** in the signup process.\n1. Go to the [Applications]({applications_url}) page and select [Register Application]({register_application_url}) and set the fields to the following values:\n * **Application ID**: Home Assistant (or any other name that makes sense)\n * **OAuth Flow**: Authorization Code Grant Flow\n * **Redirect URI**: `{redirect_url}`\n\nIn the newly created application's details, you will find the **Client ID** and the **Client Secret**."
},
"common": {
"confirmed": "Confirmed",
"present": "Present"
@@ -14,9 +11,6 @@
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Home Connect integration needs to re-authenticate your account"
},
"oauth_discovery": {
"description": "Home Assistant has found a Home Connect device on your network. Be aware that the setup of Home Connect is more complicated than many other integrations. Press **Submit** to continue setting up Home Connect."
}
},
"abort": {
@@ -162,6 +156,28 @@
}
}
},
"deprecated_program_switch_in_automations_scripts": {
"title": "Deprecated program switch detected in some automations or scripts",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch_in_automations_scripts::title%]",
"description": "Program switches are deprecated and {entity_id} is used in the following automations or scripts:\n{items}\n\nYou can use the active program select entity to run the program without any additional options and get the current running program on the above automations or scripts to fix this issue."
}
}
}
},
"deprecated_program_switch": {
"title": "Deprecated program switch entities",
"fix_flow": {
"step": {
"confirm": {
"title": "[%key:component::home_connect::issues::deprecated_program_switch::title%]",
"description": "The switch entity `{entity_id}` and all the other program switches are deprecated.\n\nPlease use the active program select entity instead."
}
}
}
},
"deprecated_set_program_and_option_actions": {
"title": "The executed action is deprecated",
"fix_flow": {

View File

@@ -4,10 +4,8 @@ import asyncio
from collections.abc import Callable, Coroutine
import itertools as it
import logging
import struct
from typing import Any
import aiofiles
import voluptuous as vol
from homeassistant import config as conf_util, core_config
@@ -33,21 +31,14 @@ from homeassistant.core import (
split_entity_id,
)
from homeassistant.exceptions import HomeAssistantError, Unauthorized, UnknownUser
from homeassistant.helpers import (
config_validation as cv,
issue_registry as ir,
recorder,
restore_state,
)
from homeassistant.helpers import config_validation as cv, recorder, restore_state
from homeassistant.helpers.entity_component import async_update_entity
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.service import (
async_extract_config_entry_ids,
async_extract_referenced_entity_ids,
async_register_admin_service,
)
from homeassistant.helpers.signal import KEY_HA_STOP
from homeassistant.helpers.system_info import async_get_system_info
from homeassistant.helpers.template import async_load_custom_templates
from homeassistant.helpers.typing import ConfigType
@@ -90,22 +81,6 @@ SCHEMA_RESTART = vol.Schema({vol.Optional(ATTR_SAFE_MODE, default=False): bool})
SHUTDOWN_SERVICES = (SERVICE_HOMEASSISTANT_STOP, SERVICE_HOMEASSISTANT_RESTART)
DEPRECATION_URL = (
"https://www.home-assistant.io/blog/2025/05/22/"
"deprecating-core-and-supervised-installation-methods-and-32-bit-systems/"
)
def _is_32_bit() -> bool:
size = struct.calcsize("P")
return size * 8 == 32
async def _get_arch() -> str:
async with aiofiles.open("/etc/apk/arch") as arch_file:
raw_arch = (await arch_file.read()).strip()
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
"""Set up general services related to Home Assistant."""
@@ -411,46 +386,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
hass.data[DATA_EXPOSED_ENTITIES] = exposed_entities
async_set_stop_handler(hass, _async_stop)
info = await async_get_system_info(hass)
installation_type = info["installation_type"][15:]
if installation_type in {"Core", "Container"}:
deprecated_method = installation_type == "Core"
bit32 = _is_32_bit()
arch = info["arch"]
if bit32 and installation_type == "Container":
arch = await _get_arch()
ir.async_create_issue(
hass,
DOMAIN,
"deprecated_container",
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_container",
translation_placeholders={"arch": arch},
)
deprecated_architecture = bit32 and installation_type != "Container"
if deprecated_method or deprecated_architecture:
issue_id = "deprecated"
if deprecated_method:
issue_id += "_method"
if deprecated_architecture:
issue_id += "_architecture"
ir.async_create_issue(
hass,
DOMAIN,
issue_id,
learn_more_url=DEPRECATION_URL,
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key=issue_id,
translation_placeholders={
"installation_type": installation_type,
"arch": arch,
},
)
return True

View File

@@ -18,13 +18,9 @@
"title": "The {integration_title} YAML configuration is being removed",
"description": "Configuring {integration_title} using YAML is being removed.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
},
"deprecated_system_packages_config_flow_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove all \"{integration_title}\" config entries."
},
"deprecated_system_packages_yaml_integration": {
"title": "The {integration_title} integration is being removed",
"description": "The {integration_title} integration is being removed as it depends on system packages that can only be installed on systems running a deprecated architecture. To resolve this, remove the {domain} entry from your configuration.yaml file and restart Home Assistant."
"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."
},
"historic_currency": {
"title": "The configured currency is no longer in use",
@@ -94,30 +90,6 @@
}
}
}
},
"deprecated_method": {
"title": "Deprecation notice: Installation method",
"description": "This system is using the {installation_type} installation type, which has been deprecated and will become unsupported following the release of Home Assistant 2025.12. While you can continue using your current setup after that point, we strongly recommend migrating to a supported installation method."
},
"deprecated_method_architecture": {
"title": "Deprecation notice",
"description": "This system is using the {installation_type} installation type, and 32-bit hardware (`{arch}`), both of which have been deprecated and will no longer be supported after the release of Home Assistant 2025.12."
},
"deprecated_architecture": {
"title": "Deprecation notice: 32-bit architecture",
"description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
},
"deprecated_container": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
"description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
},
"deprecated_os_aarch64": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. To continue using Home Assistant on this hardware, you will need to install a 64-bit operating system. Please refer to our [installation guide]({installation_guide})."
},
"deprecated_os_armv7": {
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
}
},
"system_health": {

View File

@@ -112,7 +112,6 @@ class HomematicipHAP:
self.config_entry = config_entry
self._ws_close_requested = False
self._ws_connection_closed = asyncio.Event()
self._retry_task: asyncio.Task | None = None
self._tries = 0
self._accesspoint_connected = True
@@ -128,7 +127,6 @@ class HomematicipHAP:
self.config_entry.data.get(HMIPC_AUTHTOKEN),
self.config_entry.data.get(HMIPC_NAME),
)
except HmipcConnectionError as err:
raise ConfigEntryNotReady from err
except Exception as err: # noqa: BLE001
@@ -211,13 +209,39 @@ class HomematicipHAP:
for device in self.home.devices:
device.fire_update_event()
async def async_connect(self, home: AsyncHome) -> None:
"""Connect to HomematicIP Cloud Websocket."""
await home.enable_events()
async def async_connect(self) -> None:
"""Start WebSocket connection."""
tries = 0
while True:
retry_delay = 2 ** min(tries, 8)
home.set_on_connected_handler(self.ws_connected_handler)
home.set_on_disconnected_handler(self.ws_disconnected_handler)
home.set_on_reconnect_handler(self.ws_reconnected_handler)
try:
await self.home.get_current_state_async()
hmip_events = self.home.enable_events()
tries = 0
await hmip_events
except HmipConnectionError:
_LOGGER.error(
(
"Error connecting to HomematicIP with HAP %s. "
"Retrying in %d seconds"
),
self.config_entry.unique_id,
retry_delay,
)
if self._ws_close_requested:
break
self._ws_close_requested = False
tries += 1
try:
self._retry_task = self.hass.async_create_task(
asyncio.sleep(retry_delay)
)
await self._retry_task
except asyncio.CancelledError:
break
async def async_reset(self) -> bool:
"""Close the websocket connection."""
@@ -243,26 +267,6 @@ class HomematicipHAP:
"Reset connection to access point id %s", self.config_entry.unique_id
)
async def ws_connected_handler(self) -> None:
"""Handle websocket connected."""
_LOGGER.info("Websocket connection to HomematicIP Cloud established")
if self._ws_connection_closed.is_set():
await self.get_state()
self._ws_connection_closed.clear()
async def ws_disconnected_handler(self) -> None:
"""Handle websocket disconnection."""
_LOGGER.warning("Websocket connection to HomematicIP Cloud closed")
self._ws_connection_closed.set()
async def ws_reconnected_handler(self, reason: str) -> None:
"""Handle websocket reconnection."""
_LOGGER.info(
"Websocket connection to HomematicIP Cloud re-established due to reason: %s",
reason,
)
self._ws_connection_closed.set()
async def get_hap(
self,
hass: HomeAssistant,
@@ -286,7 +290,6 @@ class HomematicipHAP:
raise HmipcConnectionError from err
home.on_update(self.async_update)
home.on_create(self.async_create_entity)
await self.async_connect(home)
hass.loop.create_task(self.async_connect())
return home

View File

@@ -4,16 +4,16 @@ from __future__ import annotations
from typing import Any
from homematicip.base.enums import DeviceType, OpticalSignalBehaviour, RGBColorState
from homematicip.base.enums import OpticalSignalBehaviour, RGBColorState
from homematicip.base.functionalChannels import NotificationLightChannel
from homematicip.device import (
BrandDimmer,
BrandSwitchMeasuring,
BrandSwitchNotificationLight,
Dimmer,
DinRailDimmer3,
FullFlushDimmer,
PluggableDimmer,
SwitchMeasuring,
WiredDimmer3,
)
from packaging.version import Version
@@ -44,12 +44,9 @@ async def async_setup_entry(
hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = []
for device in hap.home.devices:
if (
isinstance(device, SwitchMeasuring)
and getattr(device, "deviceType", None) == DeviceType.BRAND_SWITCH_MEASURING
):
if isinstance(device, BrandSwitchMeasuring):
entities.append(HomematicipLightMeasuring(hap, device))
if isinstance(device, BrandSwitchNotificationLight):
elif isinstance(device, BrandSwitchNotificationLight):
device_version = Version(device.firmwareVersion)
entities.append(HomematicipLight(hap, device))

Some files were not shown because too many files have changed in this diff Show More