Compare commits

..

6 Commits

Author SHA1 Message Date
J. Nick Koston
77bf5871cb move tests 2025-10-13 12:55:55 -10:00
J. Nick Koston
4a032e5831 cleanup 2025-10-13 12:49:49 -10:00
J. Nick Koston
b2ed6895f7 cleanup 2025-10-13 12:48:53 -10:00
J. Nick Koston
efc1df4945 Update homeassistant/data_entry_flow.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-13 12:36:37 -10:00
J. Nick Koston
b69ed592bb move it to bottom level 2025-10-13 12:00:51 -10:00
J. Nick Koston
5a168662b2 Allow passing next_flow for aborted flows 2025-10-13 11:50:16 -10:00
539 changed files with 4113 additions and 40478 deletions

View File

@@ -326,7 +326,7 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Cosign
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
uses: sigstore/cosign-installer@d7543c93d881b35a8faa02e8e3605f69b7a1ce62 # v3.10.0
with:
cosign-release: "v2.2.3"

View File

@@ -42,7 +42,7 @@ env:
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.11"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13', '3.14']"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support

4
CODEOWNERS generated
View File

@@ -619,8 +619,6 @@ build.json @home-assistant/supervisor
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
/tests/components/group/ @home-assistant/core
/homeassistant/components/growatt_server/ @johanzander
/tests/components/growatt_server/ @johanzander
/homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @tr4nt0r
@@ -1139,8 +1137,6 @@ build.json @home-assistant/supervisor
/tests/components/opengarage/ @danielhiversen
/homeassistant/components/openhome/ @bazwilliams
/tests/components/openhome/ @bazwilliams
/homeassistant/components/openrgb/ @felipecrs
/tests/components/openrgb/ @felipecrs
/homeassistant/components/opensky/ @joostlek
/tests/components/opensky/ @joostlek
/homeassistant/components/opentherm_gw/ @mvn23

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
import logging
from airos.airos8 import AirOS8
from homeassistant.const import (
@@ -14,11 +12,10 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, SECTION_ADVANCED_SETTINGS
from .coordinator import AirOSConfigEntry, AirOSDataUpdateCoordinator
_PLATFORMS: list[Platform] = [
@@ -26,8 +23,6 @@ _PLATFORMS: list[Platform] = [
Platform.SENSOR,
]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Set up Ubiquiti airOS from a config entry."""
@@ -59,13 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> boo
async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> bool:
"""Migrate old config entry."""
# This means the user has downgraded from a future version
if entry.version > 2:
if entry.version > 1:
# This means the user has downgraded from a future version
return False
# 1.1 Migrate config_entry to add advanced ssl settings
if entry.version == 1 and entry.minor_version == 1:
new_minor_version = 2
new_data = {**entry.data}
advanced_data = {
CONF_SSL: DEFAULT_SSL,
@@ -76,52 +69,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AirOSConfigEntry) -> b
hass.config_entries.async_update_entry(
entry,
data=new_data,
minor_version=new_minor_version,
)
# 2.1 Migrate binary_sensor entity unique_id from device_id to mac_address
# Step 1 - migrate binary_sensor entity unique_id
# Step 2 - migrate device entity identifier
if entry.version == 1:
new_version = 2
new_minor_version = 1
mac_adress = dr.format_mac(entry.unique_id)
device_registry = dr.async_get(hass)
if device_entry := device_registry.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, mac_adress)}
):
old_device_id = next(
(
device_id
for domain, device_id in device_entry.identifiers
if domain == DOMAIN
),
)
@callback
def update_unique_id(
entity_entry: er.RegistryEntry,
) -> dict[str, str] | None:
"""Update unique id from device_id to mac address."""
if old_device_id and entity_entry.unique_id.startswith(old_device_id):
suffix = entity_entry.unique_id.removeprefix(old_device_id)
new_unique_id = f"{mac_adress}{suffix}"
return {"new_unique_id": new_unique_id}
return None
await er.async_migrate_entries(hass, entry.entry_id, update_unique_id)
new_identifiers = device_entry.identifiers.copy()
new_identifiers.discard((DOMAIN, old_device_id))
new_identifiers.add((DOMAIN, mac_adress))
device_registry.async_update_device(
device_entry.id, new_identifiers=new_identifiers
)
hass.config_entries.async_update_entry(
entry, version=new_version, minor_version=new_minor_version
minor_version=2,
)
return True

View File

@@ -98,7 +98,7 @@ class AirOSBinarySensor(AirOSEntity, BinarySensorEntity):
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.derived.mac}_{description.key}"
self._attr_unique_id = f"{coordinator.data.host.device_id}_{description.key}"
@property
def is_on(self) -> bool:

View File

@@ -15,12 +15,7 @@ from airos.exceptions import (
)
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -62,8 +57,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Ubiquiti airOS."""
VERSION = 2
MINOR_VERSION = 1
VERSION = 1
MINOR_VERSION = 2
def __init__(self) -> None:
"""Initialize the config flow."""
@@ -124,7 +119,7 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(airos_data.derived.mac)
if self.source in [SOURCE_REAUTH, SOURCE_RECONFIGURE]:
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
else:
self._abort_if_unique_id_configured()
@@ -169,54 +164,3 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=self.errors,
)
async def async_step_reconfigure(
self,
user_input: Mapping[str, Any] | None = None,
) -> ConfigFlowResult:
"""Handle reconfiguration of airOS."""
self.errors = {}
entry = self._get_reconfigure_entry()
current_data = entry.data
if user_input is not None:
validate_data = {**current_data, **user_input}
if await self._validate_and_get_device_info(config_data=validate_data):
return self.async_update_reload_and_abort(
entry,
data_updates=validate_data,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=vol.Schema(
{
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
vol.Required(SECTION_ADVANCED_SETTINGS): section(
vol.Schema(
{
vol.Required(
CONF_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_SSL
],
): bool,
vol.Required(
CONF_VERIFY_SSL,
default=current_data[SECTION_ADVANCED_SETTINGS][
CONF_VERIFY_SSL
],
): bool,
}
),
{"collapsed": True},
),
}
),
errors=self.errors,
)

View File

@@ -33,7 +33,7 @@ class AirOSEntity(CoordinatorEntity[AirOSDataUpdateCoordinator]):
self._attr_device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, airos_data.derived.mac)},
configuration_url=configuration_url,
identifiers={(DOMAIN, airos_data.derived.mac)},
identifiers={(DOMAIN, str(airos_data.host.device_id))},
manufacturer=MANUFACTURER,
model=airos_data.host.devmodel,
model_id=(

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airos",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"quality_scale": "bronze",
"requirements": ["airos==0.5.6"]
}

View File

@@ -32,11 +32,11 @@ rules:
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
entity-unavailable: todo
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
@@ -48,9 +48,9 @@ rules:
docs-examples: todo
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-supported-functions: todo
docs-troubleshooting: done
docs-use-cases: done
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
@@ -60,7 +60,7 @@ rules:
icon-translations:
status: exempt
comment: no (custom) icons used or envisioned
reconfiguration-flow: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo

View File

@@ -10,27 +10,6 @@
"password": "[%key:component::airos::config::step::user::data_description::password%]"
}
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::airos::config::step::user::data_description::password%]"
},
"sections": {
"advanced_settings": {
"name": "[%key:component::airos::config::step::user::sections::advanced_settings::name%]",
"data": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data::ssl%]",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
},
"data_description": {
"ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::ssl%]",
"verify_ssl": "[%key:component::airos::config::step::user::sections::advanced_settings::data_description::verify_ssl%]"
}
}
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -44,7 +23,6 @@
},
"sections": {
"advanced_settings": {
"name": "Advanced settings",
"data": {
"ssl": "Use HTTPS",
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
@@ -66,7 +44,6 @@
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
}
},

View File

@@ -29,7 +29,7 @@
},
"data_description": {
"return_average": "air-Q allows to poll both the noisy sensor readings as well as the values averaged on the device (default)",
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behavior is to clip such values to 0"
"clip_negatives": "For baseline calibration purposes, certain sensor values may briefly become negative. The default behaviour is to clip such values to 0"
}
}
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==6.4.4"]
"requirements": ["aioamazondevices==6.4.3"]
}

View File

@@ -5,9 +5,14 @@ from __future__ import annotations
import asyncio
import logging
from random import randrange
import sys
from typing import Any, cast
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -24,11 +29,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
@@ -42,18 +43,6 @@ from .const import (
SIGNAL_DISCONNECTED,
)
if sys.version_info < (3, 14):
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
from pyatv.const import DeviceModel, Protocol
from pyatv.convert import model_str
from pyatv.interface import AppleTV as AppleTVInterface, DeviceListener
else:
class DeviceListener:
"""Dummy class."""
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME_TV = "Apple TV"
@@ -64,41 +53,31 @@ BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.REMOTE]
if sys.version_info < (3, 14):
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
else:
AUTH_EXCEPTIONS = ()
CONNECTION_TIMEOUT_EXCEPTIONS = ()
DEVICE_EXCEPTIONS = ()
AUTH_EXCEPTIONS = (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
)
CONNECTION_TIMEOUT_EXCEPTIONS = (
OSError,
asyncio.CancelledError,
TimeoutError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
)
DEVICE_EXCEPTIONS = (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
)
type AppleTvConfigEntry = ConfigEntry[AppleTVManager]
async def async_setup_entry(hass: HomeAssistant, entry: AppleTvConfigEntry) -> bool:
"""Set up a config entry for Apple TV."""
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Apple TV is not supported on Python 3.14. Please use Python 3.13."
)
manager = AppleTVManager(hass, entry)
if manager.is_on:

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.16.1;python_version<'3.14'"],
"requirements": ["pyatv==0.16.1"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -41,8 +41,6 @@ from .pipeline import (
async_setup_pipeline_store,
async_update_pipeline,
)
from .select import AssistPipelineSelect, VadSensitivitySelect
from .vad import VadSensitivity
from .websocket_api import async_register_websocket_api
__all__ = (
@@ -53,14 +51,11 @@ __all__ = (
"SAMPLE_CHANNELS",
"SAMPLE_RATE",
"SAMPLE_WIDTH",
"AssistPipelineSelect",
"AudioSettings",
"Pipeline",
"PipelineEvent",
"PipelineEventType",
"PipelineNotFound",
"VadSensitivity",
"VadSensitivitySelect",
"WakeWordSettings",
"async_create_default_pipeline",
"async_get_pipelines",

View File

@@ -3,17 +3,17 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import namedtuple
from collections.abc import Awaitable, Callable, Coroutine
import functools
import logging
from typing import Any, NamedTuple
from typing import Any
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aiohttp import ClientSession
from asusrouter import AsusRouter, AsusRouterError
from asusrouter.config import ARConfigKey
from asusrouter.modules.client import AsusClient
from asusrouter.modules.connection import ConnectionState
from asusrouter.modules.client import AsusClient, ConnectionState
from asusrouter.modules.data import AsusData
from asusrouter.modules.homeassistant import convert_to_ha_data, convert_to_ha_sensors
from asusrouter.tools.connection import get_cookie_jar
@@ -61,14 +61,7 @@ SENSORS_TYPE_RATES = "sensors_rates"
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
SENSORS_TYPE_UPTIME = "sensors_uptime"
class WrtDevice(NamedTuple):
"""WrtDevice structure."""
ip: str | None
name: str | None
conneted_to: str | None
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
_LOGGER = logging.getLogger(__name__)
@@ -87,7 +80,7 @@ def handle_errors_and_zip[_AsusWrtBridgeT: AsusWrtBridge](
"""Run library methods and zip results or manage exceptions."""
@functools.wraps(func)
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, str]:
async def _wrapper(self: _AsusWrtBridgeT) -> dict[str, Any]:
try:
data = await func(self)
except exceptions as exc:

View File

@@ -10,6 +10,8 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AsusWrtConfigEntry
from .router import AsusWrtDevInfo, AsusWrtRouter
ATTR_LAST_TIME_REACHABLE = "last_time_reachable"
DEFAULT_DEVICE_NAME = "Unknown device"
@@ -56,6 +58,8 @@ def add_entities(
class AsusWrtDevice(ScannerEntity):
"""Representation of a AsusWrt device."""
_unrecorded_attributes = frozenset({ATTR_LAST_TIME_REACHABLE})
_attr_should_poll = False
def __init__(self, router: AsusWrtRouter, device: AsusWrtDevInfo) -> None:
@@ -93,6 +97,11 @@ class AsusWrtDevice(ScannerEntity):
def async_on_demand_update(self) -> None:
"""Update state."""
self._device = self._router.devices[self._device.mac]
self._attr_extra_state_attributes = {}
if self._device.last_activity:
self._attr_extra_state_attributes[ATTR_LAST_TIME_REACHABLE] = (
self._device.last_activity.isoformat(timespec="seconds")
)
self.async_write_ha_state()
async def async_added_to_hass(self) -> None:

View File

@@ -136,22 +136,17 @@ class WellKnownOAuthInfoView(HomeAssistantView):
url_prefix = get_url(hass, require_current_request=True)
except NoURLAvailableError:
url_prefix = ""
metadata = {
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
),
}
# Add issuer only when we have a valid base URL (RFC 8414 compliance)
if url_prefix:
metadata["issuer"] = url_prefix
return self.json(metadata)
return self.json(
{
"authorization_endpoint": f"{url_prefix}/auth/authorize",
"token_endpoint": f"{url_prefix}/auth/token",
"revocation_endpoint": f"{url_prefix}/auth/revoke",
"response_types_supported": ["code"],
"service_documentation": (
"https://developers.home-assistant.io/docs/auth_api"
),
}
)
class AuthProvidersView(HomeAssistantView):

View File

@@ -146,7 +146,7 @@
},
"state": {
"title": "Add a Bayesian sensor",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behavior can be overridden by adding observations for the same entity's other states.",
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
"data": {
"name": "[%key:common::config_flow::data::name%]",

View File

@@ -113,6 +113,7 @@ __all__ = [
"BluetoothServiceInfo",
"BluetoothServiceInfoBleak",
"HaBluetoothConnector",
"HomeAssistantRemoteScanner",
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",

View File

@@ -3,20 +3,15 @@
from __future__ import annotations
from datetime import datetime
from functools import partial
import logging
from typing import Any
import caldav
from caldav.lib.error import DAVError
import requests
import voluptuous as vol
from homeassistant.components.calendar import (
ENTITY_ID_FORMAT,
PLATFORM_SCHEMA as CALENDAR_PLATFORM_SCHEMA,
CalendarEntity,
CalendarEntityFeature,
CalendarEvent,
is_offset_reached,
)
@@ -28,7 +23,6 @@ from homeassistant.const import (
CONF_VERIFY_SSL,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.entity_platform import (
@@ -181,8 +175,6 @@ async def async_setup_entry(
class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarEntity):
"""A device for getting the next Task from a WebDav Calendar."""
_attr_supported_features = CalendarEntityFeature.CREATE_EVENT
def __init__(
self,
name: str | None,
@@ -211,31 +203,6 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
"""Get all events in a specific time frame."""
return await self.coordinator.async_get_events(hass, start_date, end_date)
async def async_create_event(self, **kwargs: Any) -> None:
"""Create a new event in the calendar."""
_LOGGER.debug("Event: %s", kwargs)
item_data: dict[str, Any] = {
"summary": kwargs["summary"],
"dtstart": kwargs["dtstart"],
"dtend": kwargs["dtend"],
}
if description := kwargs.get("description"):
item_data["description"] = description
if location := kwargs.get("location"):
item_data["location"] = location
if rrule := kwargs.get("rrule"):
item_data["rrule"] = rrule
_LOGGER.debug("ICS data %s", item_data)
try:
await self.hass.async_add_executor_job(
partial(self.coordinator.calendar.add_event, **item_data),
)
except (requests.ConnectionError, DAVError) as err:
raise HomeAssistantError(f"CalDAV save error: {err}") from err
@callback
def _handle_coordinator_update(self) -> None:
"""Update event data."""

View File

@@ -74,10 +74,7 @@ from .const import (
StreamType,
)
from .helper import get_camera_from_entity_id
from .img_util import (
TurboJPEGSingleton, # noqa: F401
scale_jpeg_camera_image,
)
from .img_util import scale_jpeg_camera_image
from .prefs import (
CameraPreferences,
DynamicStreamSettings, # noqa: F401

View File

@@ -31,7 +31,7 @@ async def async_setup_entry(
for location_id, location in coordinator.data["locations"].items()
]
async_add_entities(alarms)
async_add_entities(alarms, True)
class CanaryAlarm(

View File

@@ -68,7 +68,8 @@ async def async_setup_entry(
for location_id, location in coordinator.data["locations"].items()
for device in location.devices
if device.is_online
)
),
True,
)

View File

@@ -80,7 +80,7 @@ async def async_setup_entry(
if device_type.get("name") in sensor_type[4]
)
async_add_entities(sensors)
async_add_entities(sensors, True)
class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity):

View File

@@ -19,7 +19,7 @@ from homeassistant.components.alexa import (
errors as alexa_errors,
smart_home as alexa_smart_home,
)
from homeassistant.components.camera import async_register_ice_servers
from homeassistant.components.camera.webrtc import async_register_ice_servers
from homeassistant.components.google_assistant import smart_home as ga
from homeassistant.const import __version__ as HA_VERSION
from homeassistant.core import Context, HassJob, HomeAssistant, callback

View File

@@ -12,9 +12,7 @@ from hass_nabucasa.google_report_state import ErrorResponse
from homeassistant.components.binary_sensor import BinarySensorDeviceClass
from homeassistant.components.google_assistant import DOMAIN as GOOGLE_DOMAIN
from homeassistant.components.google_assistant.helpers import ( # pylint: disable=hass-component-root-import
AbstractConfig,
)
from homeassistant.components.google_assistant.helpers import AbstractConfig
from homeassistant.components.homeassistant.exposed_entities import (
async_expose_entity,
async_get_assistant_settings,

View File

@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==1.4.0"],
"requirements": ["hass-nabucasa==1.3.0"],
"single_config_entry": true
}

View File

@@ -11,7 +11,7 @@ from hass_nabucasa.voice import MAP_VOICE, Gender
from homeassistant.auth.const import GROUP_ID_ADMIN
from homeassistant.auth.models import User
from homeassistant.components import webhook
from homeassistant.components.google_assistant.http import ( # pylint: disable=hass-component-root-import
from homeassistant.components.google_assistant.http import (
async_get_users as async_get_google_assistant_users,
)
from homeassistant.core import HomeAssistant, callback

View File

@@ -38,10 +38,6 @@ TYPE_SPECIFY_COUNTRY = "specify_country_code"
_LOGGER = logging.getLogger(__name__)
DESCRIPTION_PLACEHOLDER = {
"register_link": "https://electricitymaps.com/free-tier",
}
class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Co2signal."""
@@ -74,7 +70,6 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user",
data_schema=data_schema,
description_placeholders=DESCRIPTION_PLACEHOLDER,
)
data = {CONF_API_KEY: user_input[CONF_API_KEY]}
@@ -184,5 +179,4 @@ class ElectricityMapsConfigFlow(ConfigFlow, domain=DOMAIN):
step_id=step_id,
data_schema=data_schema,
errors=errors,
description_placeholders=DESCRIPTION_PLACEHOLDER,
)

View File

@@ -18,6 +18,7 @@ rules:
status: todo
comment: |
The config flow misses data descriptions.
Remove URLs from data descriptions, they should be replaced with placeholders.
Make use of Electricity Maps zone keys in country code as dropdown.
Make use of location selector for coordinates.
dependency-transparency: done

View File

@@ -6,7 +6,7 @@
"location": "[%key:common::config_flow::data::location%]",
"api_key": "[%key:common::config_flow::data::access_token%]"
},
"description": "Visit the [Electricity Maps page]({register_link}) to request a token."
"description": "Visit https://electricitymaps.com/free-tier to request a token."
},
"coordinates": {
"data": {

View File

@@ -166,7 +166,6 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"account_name": self.reauth_entry.title,
"developer_url": "https://www.coinbase.com/developer-platform",
},
errors=errors,
)
@@ -196,7 +195,6 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=STEP_USER_DATA_SCHEMA,
description_placeholders={
"account_name": self.reauth_entry.title,
"developer_url": "https://www.coinbase.com/developer-platform",
},
errors=errors,
)

View File

@@ -11,7 +11,7 @@
},
"reauth_confirm": {
"title": "Update Coinbase API credentials",
"description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit the [Developer Platform]({developer_url}) to create new credentials for {account_name}.",
"description": "Your current Coinbase API key appears to be for the deprecated v2 API. Please reconfigure with a new API key created for the v3 API. Visit https://www.coinbase.com/developer-platform to create new credentials for {account_name}.",
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]",
"api_token": "API secret"

View File

@@ -6,9 +6,7 @@ from typing import Any
import uuid
from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN
from homeassistant.components.automation.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.components.automation.config import async_validate_config_item
from homeassistant.config import AUTOMATION_CONFIG_PATH
from homeassistant.const import CONF_ID, SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -5,9 +5,7 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.script import DOMAIN as SCRIPT_DOMAIN
from homeassistant.components.script.config import ( # pylint: disable=hass-component-root-import
async_validate_config_item,
)
from homeassistant.components.script.config import async_validate_config_item
from homeassistant.config import SCRIPT_CONFIG_PATH
from homeassistant.const import SERVICE_RELOAD
from homeassistant.core import HomeAssistant, callback

View File

@@ -148,15 +148,6 @@ async def async_setup_entry(
source_type={dev_type}, idx=dev_id, name=name
)
# Skip rooms with no audio/video sources
if not sources:
_LOGGER.debug(
"Skipping room '%s' (ID: %s) - no audio/video sources found",
room.get("name"),
room_id,
)
continue
try:
hidden = room["roomHidden"]
entity_list.append(

View File

@@ -569,17 +569,14 @@ class ChatLog:
if llm_api:
prompt_parts.append(llm_api.api_prompt)
# Append current date and time to the prompt if the corresponding tool is not provided
llm_tools: list[llm.Tool] = llm_api.tools if llm_api else []
if not any(tool.name.endswith("GetDateTime") for tool in llm_tools):
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.DATE_TIME_PROMPT,
llm_context.language,
user_name,
)
prompt_parts.append(
await self._async_expand_prompt_template(
llm_context,
llm.BASE_PROMPT,
llm_context.language,
user_name,
)
)
if extra_system_prompt := (
# Take new system prompt if one was given

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
@@ -10,7 +9,7 @@ from pycync import Auth
from pycync.exceptions import AuthFailedError, CyncError, TwoFactorRequiredError
import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -40,7 +39,7 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
cync_auth: Auth = None
cync_auth: Auth
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -48,14 +47,29 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Attempt login with user credentials."""
errors: dict[str, str] = {}
if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
if not errors:
return await self._create_config_entry(self.cync_auth.username)
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login()
except AuthFailedError:
errors["base"] = "invalid_auth"
except TwoFactorRequiredError:
return await self.async_step_two_factor()
except CyncError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
@@ -67,65 +81,12 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
"""Attempt login with the two factor auth code sent to the user."""
errors: dict[str, str] = {}
if user_input:
errors = await self._validate_credentials(user_input)
if not errors:
return await self._create_config_entry(self.cync_auth.username)
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)
return self.async_show_form(
step_id="two_factor", data_schema=STEP_TWO_FACTOR_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required and prompts for their Cync credentials."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input:
try:
errors = await self._validate_credentials(user_input)
except TwoFactorRequiredError:
return await self.async_step_two_factor()
if not errors:
return await self._create_config_entry(self.cync_auth.username)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={CONF_EMAIL: reauth_entry.title},
)
async def _validate_credentials(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Attempt to log in with user email and password, and return the error dict."""
errors: dict[str, str] = {}
if not self.cync_auth:
self.cync_auth = Auth(
async_get_clientsession(self.hass),
username=user_input[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
)
try:
await self.cync_auth.login(user_input.get(CONF_TWO_FACTOR_CODE))
except TwoFactorRequiredError:
raise
await self.cync_auth.login(user_input[CONF_TWO_FACTOR_CODE])
except AuthFailedError:
errors["base"] = "invalid_auth"
except CyncError:
@@ -133,29 +94,25 @@ class CyncConfigFlow(ConfigFlow, domain=DOMAIN):
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return await self._create_config_entry(self.cync_auth.username)
return errors
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def _create_config_entry(self, user_email: str) -> ConfigFlowResult:
"""Create the Cync config entry using input user data."""
cync_user = self.cync_auth.user
await self.async_set_unique_id(str(cync_user.user_id))
self._abort_if_unique_id_configured()
config_data = {
config = {
CONF_USER_ID: cync_user.user_id,
CONF_AUTHORIZE_STRING: cync_user.authorize,
CONF_EXPIRES_AT: cync_user.expires_at,
CONF_ACCESS_TOKEN: cync_user.access_token,
CONF_REFRESH_TOKEN: cync_user.refresh_token,
}
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch()
return self.async_update_reload_and_abort(
entry=self._get_reauth_entry(), title=user_email, data=config_data
)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user_email, data=config_data)
return self.async_create_entry(title=user_email, data=config)

View File

@@ -37,7 +37,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: done
reauthentication-flow: todo
test-coverage: todo
# Gold

View File

@@ -18,18 +18,6 @@
"data_description": {
"two_factor_code": "The two-factor code sent to your Cync account's email"
}
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The Cync integration needs to re-authenticate for {email}",
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::cync::config::step::user::data_description::email%]",
"password": "[%key:component::cync::config::step::user::data_description::password%]"
}
}
},
"error": {
@@ -38,9 +26,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "An incorrect user was provided by Cync for your email address, please consult your Cync app"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
}
}
}

View File

@@ -5,9 +5,7 @@ from __future__ import annotations
import datetime
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import
ManualAlarm,
)
from homeassistant.components.manual.alarm_control_panel import ManualAlarm
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME
from homeassistant.core import HomeAssistant

View File

@@ -139,7 +139,6 @@ class DemoCover(CoverEntity):
self.async_write_ha_state()
return
self._is_opening = False
self._is_closing = True
self._listen_cover()
self._requested_closing = True
@@ -163,7 +162,6 @@ class DemoCover(CoverEntity):
return
self._is_opening = True
self._is_closing = False
self._listen_cover()
self._requested_closing = False
self.async_write_ha_state()
@@ -183,14 +181,10 @@ class DemoCover(CoverEntity):
if self._position == position:
return
self._is_closing = position < (self._position or 0)
self._is_opening = not self._is_closing
self._listen_cover()
self._requested_closing = (
self._position is not None and position < self._position
)
self.async_write_ha_state()
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover til to a specific position."""

View File

@@ -196,7 +196,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self._attr_name = name if name is not None else f"{source_entity} derivative"
self._attr_extra_state_attributes = {ATTR_SOURCE_ID: source_entity}
self._unit_template: str | None = None
if unit_of_measurement is None:
final_unit_prefix = "" if unit_prefix is None else unit_prefix
self._unit_template = f"{final_unit_prefix}{{}}/{unit_time}"
@@ -217,23 +217,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
lambda *args: None
)
def _derive_and_set_attributes_from_state(self, source_state: State | None) -> None:
if self._unit_template and source_state:
original_unit = self._attr_native_unit_of_measurement
source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if source_unit is None else source_unit
)
if original_unit != self._attr_native_unit_of_measurement:
_LOGGER.debug(
"%s: Derivative sensor switched UoM from %s to %s, resetting state to 0",
self.entity_id,
original_unit,
self._attr_native_unit_of_measurement,
)
self._state_list = []
self._attr_native_value = round(Decimal(0), self._round_digits)
def _calc_derivative_from_state_list(self, current_time: datetime) -> Decimal:
def calculate_weight(start: datetime, end: datetime, now: datetime) -> float:
window_start = now - timedelta(seconds=self._time_window)
@@ -302,9 +285,6 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
except (InvalidOperation, TypeError):
self._attr_native_value = None
source_state = self.hass.states.get(self._sensor_source_id)
self._derive_and_set_attributes_from_state(source_state)
def schedule_max_sub_interval_exceeded(source_state: State | None) -> None:
"""Schedule calculation using the source state and max_sub_interval.
@@ -378,18 +358,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
_LOGGER.debug("%s: New state changed event: %s", self.entity_id, event.data)
self._cancel_max_sub_interval_exceeded_callback()
new_state = event.data["new_state"]
if not self._handle_invalid_source_state(new_state):
return
assert new_state
original_unit = self._attr_native_unit_of_measurement
self._derive_and_set_attributes_from_state(new_state)
if original_unit != self._attr_native_unit_of_measurement:
self.async_write_ha_state()
return
schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
if old_state is not None:
@@ -419,6 +391,12 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
self.async_write_ha_state()
return
if self.native_unit_of_measurement is None:
unit = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
self._attr_native_unit_of_measurement = self._unit_template.format(
"" if unit is None else unit
)
self._prune_state_list(new_timestamp)
try:

View File

@@ -2,12 +2,12 @@
from __future__ import annotations
from homeassistant.const import STATE_HOME
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass
from .config_entry import (
from .config_entry import ( # noqa: F401
ScannerEntity,
ScannerEntityDescription,
TrackerEntity,
@@ -15,7 +15,7 @@ from .config_entry import (
async_setup_entry,
async_unload_entry,
)
from .const import (
from .const import ( # noqa: F401
ATTR_ATTRIBUTES,
ATTR_BATTERY,
ATTR_DEV_ID,
@@ -37,7 +37,7 @@ from .const import (
SCAN_INTERVAL,
SourceType,
)
from .legacy import (
from .legacy import ( # noqa: F401
PLATFORM_SCHEMA,
PLATFORM_SCHEMA_BASE,
SERVICE_SEE,
@@ -61,44 +61,3 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
async_setup_legacy_integration(hass, config)
return True
__all__ = (
"ATTR_ATTRIBUTES",
"ATTR_BATTERY",
"ATTR_DEV_ID",
"ATTR_GPS",
"ATTR_HOST_NAME",
"ATTR_IP",
"ATTR_LOCATION_NAME",
"ATTR_MAC",
"ATTR_SOURCE_TYPE",
"CONF_CONSIDER_HOME",
"CONF_NEW_DEVICE_DEFAULTS",
"CONF_SCAN_INTERVAL",
"CONF_TRACK_NEW",
"CONNECTED_DEVICE_REGISTERED",
"DEFAULT_CONSIDER_HOME",
"DEFAULT_TRACK_NEW",
"DOMAIN",
"ENTITY_ID_FORMAT",
"PLATFORM_SCHEMA",
"PLATFORM_SCHEMA_BASE",
"SCAN_INTERVAL",
"SERVICE_SEE",
"SERVICE_SEE_PAYLOAD_SCHEMA",
"SOURCE_TYPES",
"AsyncSeeCallback",
"DeviceScanner",
"ScannerEntity",
"ScannerEntityDescription",
"SeeCallback",
"SourceType",
"TrackerEntity",
"TrackerEntityDescription",
"async_setup",
"async_setup_entry",
"async_unload_entry",
"is_on",
"see",
)

View File

@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pydoods"],
"quality_scale": "legacy",
"requirements": ["pydoods==1.0.2", "Pillow==12.0.0"]
"requirements": ["pydoods==1.0.2", "Pillow==11.3.0"]
}

View File

@@ -61,8 +61,5 @@ class EcobeeFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="authorize",
errors=errors,
description_placeholders={
"pin": self._ecobee.pin,
"auth_url": "https://www.ecobee.com/consumerportal/index.html",
},
description_placeholders={"pin": self._ecobee.pin},
)

View File

@@ -8,7 +8,7 @@
}
},
"authorize": {
"description": "Please authorize this app at {auth_url} with PIN code:\n\n{pin}\n\nThen, select **Submit**."
"description": "Please authorize this app at https://www.ecobee.com/consumerportal/index.html with PIN code:\n\n{pin}\n\nThen, select **Submit**."
}
},
"error": {

View File

@@ -8,11 +8,8 @@ from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterErrorCode
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
)
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -16,9 +16,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor.recorder import ( # pylint: disable=hass-component-root-import
reset_detected,
)
from homeassistant.components.sensor.recorder import reset_detected
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfEnergy, UnitOfVolume
from homeassistant.core import (
HomeAssistant,

View File

@@ -38,25 +38,6 @@
},
"available_energy": {
"default": "mdi:battery-50"
},
"grid_status": {
"default": "mdi:transmission-tower",
"state": {
"off_grid": "mdi:transmission-tower-off",
"synchronizing": "mdi:sync-alert"
}
},
"mid_state": {
"default": "mdi:electric-switch-closed",
"state": {
"open": "mdi:electric-switch"
}
},
"admin_state": {
"default": "mdi:transmission-tower",
"state": {
"off_grid": "mdi:transmission-tower-off"
}
}
},
"switch": {

View File

@@ -824,12 +824,6 @@ class EnvoyCollarSensorEntityDescription(SensorEntityDescription):
value_fn: Callable[[EnvoyCollar], datetime.datetime | int | float | str]
# translations don't accept uppercase
ADMIN_STATE_MAP = {
"ENCMN_MDE_ON_GRID": "on_grid",
"ENCMN_MDE_OFF_GRID": "off_grid",
}
COLLAR_SENSORS = (
EnvoyCollarSensorEntityDescription(
key="temperature",
@@ -844,21 +838,11 @@ COLLAR_SENSORS = (
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda collar: dt_util.utc_from_timestamp(collar.last_report_date),
),
# grid_state does not seem to change when off-grid, but rather admin_state_str
EnvoyCollarSensorEntityDescription(
key="grid_state",
translation_key="grid_status",
value_fn=lambda collar: collar.grid_state,
),
# grid_status off-grid shows in admin_state rather than in grid_state
# map values as translations don't accept uppercase which these are
EnvoyCollarSensorEntityDescription(
key="admin_state_str",
translation_key="admin_state",
value_fn=lambda collar: ADMIN_STATE_MAP.get(
collar.admin_state_str, collar.admin_state_str
),
),
EnvoyCollarSensorEntityDescription(
key="mid_state",
translation_key="mid_state",

View File

@@ -409,26 +409,10 @@
"name": "Last report duration"
},
"grid_status": {
"name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]",
"state": {
"on_grid": "On grid",
"off_grid": "Off grid",
"synchronizing": "Synchronizing to grid"
}
"name": "[%key:component::enphase_envoy::entity::binary_sensor::grid_status::name%]"
},
"mid_state": {
"name": "MID state",
"state": {
"open": "[%key:common::state::open%]",
"close": "[%key:common::state::closed%]"
}
},
"admin_state": {
"name": "Admin state",
"state": {
"on_grid": "[%key:component::enphase_envoy::entity::sensor::grid_status::state::on_grid%]",
"off_grid": "[%key:component::enphase_envoy::entity::sensor::grid_status::state::off_grid%]"
}
"name": "MID state"
}
},
"switch": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/epson",
"iot_class": "local_polling",
"loggers": ["epson_projector"],
"requirements": ["epson-projector==0.6.0"]
"requirements": ["epson-projector==0.5.1"]
}

View File

@@ -10,8 +10,8 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.components.sensor.const import SensorStateClass
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

View File

@@ -16,7 +16,6 @@ from aioesphomeapi import (
InvalidEncryptionKeyAPIError,
RequiresEncryptionAPIError,
ResolveAPIError,
wifi_mac_to_bluetooth_mac,
)
import aiohttp
import voluptuous as vol
@@ -38,7 +37,6 @@ from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow, FlowResultType
from homeassistant.helpers import discovery_flow
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.importlib import async_import_module
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
@@ -319,24 +317,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
# Check if already configured
await self.async_set_unique_id(mac_address)
# Convert WiFi MAC to Bluetooth MAC and notify Improv BLE if waiting
# ESPHome devices use WiFi MAC + 1 for Bluetooth MAC
# Late import to avoid circular dependency
# NOTE: Do not change to hass.config.components check - improv_ble is
# config_flow only and may not be in the components registry
if improv_ble := await async_import_module(
self.hass, "homeassistant.components.improv_ble"
):
ble_mac = wifi_mac_to_bluetooth_mac(mac_address)
improv_ble.async_register_next_flow(self.hass, ble_mac, self.flow_id)
_LOGGER.debug(
"Notified Improv BLE of flow %s for BLE MAC %s (derived from WiFi MAC %s)",
self.flow_id,
ble_mac,
mac_address,
)
await self._async_validate_mac_abort_configured(
mac_address, self._host, self._port
)
@@ -520,16 +500,6 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle creating a new entry by removing the old one and creating new."""
assert self._entry_with_name_conflict is not None
if self.source in (SOURCE_REAUTH, SOURCE_RECONFIGURE):
return self.async_update_reload_and_abort(
self._entry_with_name_conflict,
title=self._name,
unique_id=self.unique_id,
data=self._async_make_config_data(),
options={
CONF_ALLOW_SERVICE_CALLS: DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
},
)
await self.hass.config_entries.async_remove(
self._entry_with_name_conflict.entry_id
)

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==42.0.0",
"aioesphomeapi==41.15.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.4.0"
],

View File

@@ -6,7 +6,7 @@ from dataclasses import replace
from aioesphomeapi import EntityInfo, SelectInfo, SelectState
from homeassistant.components.assist_pipeline import (
from homeassistant.components.assist_pipeline.select import (
AssistPipelineSelect,
VadSensitivitySelect,
)

View File

@@ -19,9 +19,7 @@ from homeassistant.components.ffmpeg import (
FFmpegManager,
get_ffmpeg_manager,
)
from homeassistant.components.ffmpeg_motion.binary_sensor import ( # pylint: disable=hass-component-root-import
FFmpegBinarySensor,
)
from homeassistant.components.ffmpeg_motion.binary_sensor import FFmpegBinarySensor
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv

View File

@@ -2,14 +2,14 @@
from __future__ import annotations
from pyfirefly.models import Account, Category
from yarl import URL
from homeassistant.const import CONF_URL
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER, NAME
from .const import DOMAIN, MANUFACTURER
from .coordinator import FireflyDataUpdateCoordinator
@@ -21,65 +21,20 @@ class FireflyBaseEntity(CoordinatorEntity[FireflyDataUpdateCoordinator]):
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
entity_description: EntityDescription,
) -> None:
"""Initialize a Firefly entity."""
super().__init__(coordinator)
self.entity_description = entity_description
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=NAME,
configuration_url=URL(coordinator.config_entry.data[CONF_URL]),
identifiers={(DOMAIN, f"{coordinator.config_entry.entry_id}_service")},
)
class FireflyAccountBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III account entity."""
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
account: Account,
key: str,
) -> None:
"""Initialize a Firefly account entity."""
super().__init__(coordinator)
self._account = account
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=account.attributes.name,
configuration_url=f"{URL(coordinator.config_entry.data[CONF_URL])}/accounts/show/{account.id}",
identifiers={
(DOMAIN, f"{coordinator.config_entry.entry_id}_account_{account.id}")
(
DOMAIN,
f"{coordinator.config_entry.entry_id}_{self.entity_description.key}",
)
},
)
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_account_{account.id}_{key}"
)
class FireflyCategoryBaseEntity(FireflyBaseEntity):
"""Base class for Firefly III category entity."""
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
category: Category,
key: str,
) -> None:
"""Initialize a Firefly category entity."""
super().__init__(coordinator)
self._category = category
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
manufacturer=MANUFACTURER,
name=category.attributes.name,
configuration_url=f"{URL(coordinator.config_entry.data[CONF_URL])}/categories/show/{category.id}",
identifiers={
(DOMAIN, f"{coordinator.config_entry.entry_id}_category_{category.id}")
},
)
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}_category_{category.id}_{key}"
)

View File

@@ -2,13 +2,13 @@
"entity": {
"sensor": {
"account_type": {
"default": "mdi:bank"
},
"account_balance": {
"default": "mdi:currency-usd"
},
"account_role": {
"default": "mdi:account-circle"
"default": "mdi:bank",
"state": {
"expense": "mdi:cash-minus",
"revenue": "mdi:cash-plus",
"asset": "mdi:account-cash",
"liability": "mdi:hand-coin"
}
},
"category": {
"default": "mdi:label"

View File

@@ -5,36 +5,34 @@ from __future__ import annotations
from pyfirefly.models import Account, Category
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
StateType,
)
from homeassistant.const import EntityCategory
from homeassistant.components.sensor.const import SensorDeviceClass
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import FireflyConfigEntry, FireflyDataUpdateCoordinator
from .entity import FireflyAccountBaseEntity, FireflyCategoryBaseEntity
from .entity import FireflyBaseEntity
ACCOUNT_ROLE_MAPPING = {
"defaultAsset": "default_asset",
"sharedAsset": "shared_asset",
"savingAsset": "saving_asset",
"ccAsset": "cc_asset",
"cashWalletAsset": "cash_wallet_asset",
}
ACCOUNT_TYPE_ICONS = {
"expense": "mdi:cash-minus",
"asset": "mdi:account-cash",
"revenue": "mdi:cash-plus",
"liability": "mdi:hand-coin",
}
ACCOUNT_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="account_type",
translation_key="account",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL,
),
)
ACCOUNT_BALANCE = "account_balance"
ACCOUNT_ROLE = "account_role"
ACCOUNT_TYPE = "account_type"
CATEGORY = "category"
CATEGORY_SENSORS: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
key="category",
translation_key="category",
device_class=SensorDeviceClass.MONETARY,
state_class=SensorStateClass.TOTAL,
),
)
async def async_setup_entry(
@@ -42,137 +40,94 @@ async def async_setup_entry(
entry: FireflyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Firefly III sensors."""
"""Set up the Firefly III sensor platform."""
coordinator = entry.runtime_data
entities: list[SensorEntity] = []
for account in coordinator.data.accounts:
entities.append(
FireflyAccountBalanceSensor(coordinator, account, ACCOUNT_BALANCE)
entities: list[SensorEntity] = [
FireflyAccountEntity(
coordinator=coordinator,
entity_description=description,
account=account,
)
entities.append(FireflyAccountRoleSensor(coordinator, account, ACCOUNT_ROLE))
entities.append(FireflyAccountTypeSensor(coordinator, account, ACCOUNT_TYPE))
for account in coordinator.data.accounts
for description in ACCOUNT_SENSORS
]
entities.extend(
[
FireflyCategorySensor(coordinator, category, CATEGORY)
for category in coordinator.data.category_details
]
FireflyCategoryEntity(
coordinator=coordinator,
entity_description=description,
category=category,
)
for category in coordinator.data.category_details
for description in CATEGORY_SENSORS
)
async_add_entities(entities)
class FireflyAccountBalanceSensor(FireflyAccountBaseEntity, SensorEntity):
"""Account balance sensor."""
_attr_translation_key = "account_balance"
_attr_device_class = SensorDeviceClass.MONETARY
_attr_state_class = SensorStateClass.TOTAL
class FireflyAccountEntity(FireflyBaseEntity, SensorEntity):
"""Entity for Firefly III account."""
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
entity_description: SensorEntityDescription,
account: Account,
key: str,
) -> None:
"""Initialize the account balance sensor."""
super().__init__(coordinator, account, key)
"""Initialize Firefly account entity."""
super().__init__(coordinator, entity_description)
self._account = account
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{entity_description.key}_{account.id}"
self._attr_name = account.attributes.name
self._attr_native_unit_of_measurement = (
coordinator.data.primary_currency.attributes.code
)
# Account type state doesn't go well with the icons.json. Need to fix it.
if account.attributes.type == "expense":
self._attr_icon = "mdi:cash-minus"
elif account.attributes.type == "asset":
self._attr_icon = "mdi:account-cash"
elif account.attributes.type == "revenue":
self._attr_icon = "mdi:cash-plus"
elif account.attributes.type == "liability":
self._attr_icon = "mdi:hand-coin"
else:
self._attr_icon = "mdi:bank"
@property
def native_value(self) -> StateType:
"""Return current account balance."""
def native_value(self) -> str | None:
"""Return the state of the sensor."""
return self._account.attributes.current_balance
class FireflyAccountRoleSensor(FireflyAccountBaseEntity, SensorEntity):
"""Account role diagnostic sensor."""
_attr_translation_key = "account_role"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = True
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
account: Account,
key: str,
) -> None:
"""Initialize the account role sensor."""
super().__init__(coordinator, account, key)
self._account = account
@property
def native_value(self) -> StateType:
"""Return account role."""
# An account can be empty and then should resort to Unknown
account_role: str | None = self._account.attributes.account_role
if account_role is None:
return None
return ACCOUNT_ROLE_MAPPING.get(account_role, account_role)
class FireflyAccountTypeSensor(FireflyAccountBaseEntity, SensorEntity):
"""Account type diagnostic sensor."""
_attr_translation_key = "account_type"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = True
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
account: Account,
key: str,
) -> None:
"""Initialize the account type sensor."""
super().__init__(coordinator, account, key)
acc_type = account.attributes.type
self._attr_icon = (
ACCOUNT_TYPE_ICONS.get(acc_type, "mdi:bank")
if acc_type is not None
else "mdi:bank"
)
@property
def native_value(self) -> StateType:
"""Return account type."""
return self._account.attributes.type
class FireflyCategorySensor(FireflyCategoryBaseEntity, SensorEntity):
"""Category sensor."""
_attr_translation_key = "category"
_attr_device_class = SensorDeviceClass.MONETARY
_attr_state_class = SensorStateClass.TOTAL
class FireflyCategoryEntity(FireflyBaseEntity, SensorEntity):
"""Entity for Firefly III category."""
def __init__(
self,
coordinator: FireflyDataUpdateCoordinator,
entity_description: SensorEntityDescription,
category: Category,
key: str,
) -> None:
"""Initialize the category sensor."""
super().__init__(coordinator, category, key)
"""Initialize Firefly category entity."""
super().__init__(coordinator, entity_description)
self._category = category
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{entity_description.key}_{category.id}"
self._attr_name = category.attributes.name
self._attr_native_unit_of_measurement = (
coordinator.data.primary_currency.attributes.code
)
@property
def native_value(self) -> StateType:
"""Return net spent+earned value for this category in the period."""
def native_value(self) -> float | None:
"""Return the state of the sensor."""
spent_items = self._category.attributes.spent or []
earned_items = self._category.attributes.earned or []
spent = sum(float(item.sum) for item in spent_items if item.sum is not None)
earned = sum(float(item.sum) for item in earned_items if item.sum is not None)
if spent == 0 and earned == 0:
return None
return spent + earned

View File

@@ -45,34 +45,5 @@
"timeout_connect": {
"message": "A timeout occurred while trying to connect to the Firefly instance: {error}"
}
},
"entity": {
"sensor": {
"account_balance": {
"name": "Account Balance"
},
"account_role": {
"name": "Account Role",
"state": {
"default_asset": "Default asset",
"shared_asset": "Shared asset",
"saving_asset": "Saving asset",
"cc_asset": "Credit card asset",
"cash_wallet_asset": "Cash wallet asset"
}
},
"account_type": {
"name": "Account Type",
"state": {
"asset": "Asset",
"expense": "Expense",
"revenue": "Revenue",
"liability": "Liability"
}
},
"category": {
"name": "Earned/Spent"
}
}
}
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/foscam",
"iot_class": "local_polling",
"loggers": ["libpyfoscamcgi"],
"requirements": ["libpyfoscamcgi==0.0.8"]
"requirements": ["libpyfoscamcgi==0.0.7"]
}

View File

@@ -6,8 +6,9 @@ import logging
from typing import Any
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS, CONF_INPUT
from homeassistant.components.ffmpeg.camera import ( # pylint: disable=hass-component-root-import
from homeassistant.components.ffmpeg.camera import (
CONF_EXTRA_ARGUMENTS,
CONF_INPUT,
DEFAULT_ARGUMENTS,
FFmpegCamera,
)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import NotRequired, TypedDict
from typing import TypedDict
from homeassistant.util import dt as dt_util
@@ -55,7 +55,7 @@ HostAttributes = TypedDict(
"X_AVM-DE_Guest": bool,
"X_AVM-DE_RequestClient": str,
"X_AVM-DE_VPN": bool,
"X_AVM-DE_WANAccess": NotRequired[str],
"X_AVM-DE_WANAccess": str,
"X_AVM-DE_Disallow": bool,
"X_AVM-DE_IsMeshable": str,
"X_AVM-DE_Priority": str,

View File

@@ -453,7 +453,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass.http.app.router.register_resource(IndexView(repo_path, hass))
async_register_built_in_panel(hass, "light")
async_register_built_in_panel(hass, "safety")
async_register_built_in_panel(hass, "security")
async_register_built_in_panel(hass, "climate")
async_register_built_in_panel(hass, "profile")

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/generic",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["av==13.1.0", "Pillow==12.0.0"]
"requirements": ["av==13.1.0", "Pillow==11.3.0"]
}

View File

@@ -30,8 +30,8 @@ from homeassistant.components.camera import (
WebRTCMessage,
WebRTCSendMessage,
async_register_webrtc_provider,
get_dynamic_camera_stream_settings,
)
from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings
from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN
from homeassistant.components.stream import Orientation
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry

View File

@@ -282,7 +282,6 @@ class CoverGroup(GroupEntity, CoverEntity):
self._attr_is_closed = True
self._attr_is_closing = False
self._attr_is_opening = False
self._update_assumed_state_from_members()
for entity_id in self._entity_ids:
if not (state := self.hass.states.get(entity_id)):
continue

View File

@@ -115,17 +115,6 @@ class GroupEntity(Entity):
def async_update_group_state(self) -> None:
"""Abstract method to update the entity."""
@callback
def _update_assumed_state_from_members(self) -> None:
"""Update assumed_state based on member entities."""
self._attr_assumed_state = False
for entity_id in self._entity_ids:
if (state := self.hass.states.get(entity_id)) is None:
continue
if state.attributes.get(ATTR_ASSUMED_STATE):
self._attr_assumed_state = True
return
@callback
def async_update_supported_features(
self,

View File

@@ -252,7 +252,6 @@ class FanGroup(GroupEntity, FanEntity):
@callback
def async_update_group_state(self) -> None:
"""Update state and attributes."""
self._update_assumed_state_from_members()
states = [
state

View File

@@ -205,8 +205,6 @@ class LightGroup(GroupEntity, LightEntity):
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the light group state."""
self._update_assumed_state_from_members()
states = [
state
for entity_id in self._entity_ids

View File

@@ -156,8 +156,6 @@ class SwitchGroup(GroupEntity, SwitchEntity):
@callback
def async_update_group_state(self) -> None:
"""Query all members and determine the switch group state."""
self._update_assumed_state_from_members()
states = [
state.state
for entity_id in self._entity_ids

View File

@@ -1,7 +1,7 @@
{
"domain": "growatt_server",
"name": "Growatt",
"codeowners": ["@johanzander"],
"codeowners": [],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/growatt_server",
"iot_class": "cloud_polling",

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["habiticalib"],
"quality_scale": "platinum",
"requirements": ["habiticalib==0.4.6"]
"requirements": ["habiticalib==0.4.5"]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.hardware import BoardInfo, HardwareInfo
from homeassistant.components.hardware.models import BoardInfo, HardwareInfo
from homeassistant.components.hassio import get_os_info
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError

View File

@@ -11,13 +11,7 @@ from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import DATA_HARDWARE, DOMAIN
from .hardware import async_process_hardware_platforms
from .models import BoardInfo, HardwareData, HardwareInfo, SystemStatus, USBInfo
__all__ = [
"BoardInfo",
"HardwareInfo",
"USBInfo",
]
from .models import HardwareData, SystemStatus
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)

View File

@@ -1,20 +1,15 @@
"""The Logitech Harmony Hub integration."""
from __future__ import annotations
import logging
import sys
from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS
from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
if sys.version_info < (3, 14):
from .const import HARMONY_OPTIONS_UPDATE, PLATFORMS
from .data import HarmonyConfigEntry, HarmonyData
from .const import HARMONY_OPTIONS_UPDATE, PLATFORMS
from .data import HarmonyConfigEntry, HarmonyData
_LOGGER = logging.getLogger(__name__)
@@ -25,10 +20,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HarmonyConfigEntry) -> b
# when setting up a config entry, we fallback to adding
# the options to the config entry and pull them out here if
# they are missing from the options
if sys.version_info >= (3, 14):
raise HomeAssistantError(
"Logitech Harmony Hub is not supported on Python 3.14. Please use Python 3.13."
)
_async_import_options_from_data_if_missing(hass, entry)
address = entry.data[CONF_HOST]

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/harmony",
"iot_class": "local_push",
"loggers": ["aioharmony", "slixmpp"],
"requirements": ["aioharmony==0.5.3;python_version<'3.14'"],
"requirements": ["aioharmony==0.5.3"],
"ssdp": [
{
"manufacturer": "Logitech",

View File

@@ -22,6 +22,6 @@
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
"quality_scale": "platinum",
"requirements": ["aiohomeconnect==0.22.0"],
"requirements": ["aiohomeconnect==0.20.0"],
"zeroconf": ["_homeconnect._tcp.local."]
}

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.hardware import HardwareInfo, USBInfo
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
from homeassistant.core import HomeAssistant, callback
from .config_flow import HomeAssistantConnectZBT2ConfigFlow

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.hardware import BoardInfo, HardwareInfo
from homeassistant.components.hardware.models import BoardInfo, HardwareInfo
from homeassistant.components.hassio import get_os_info
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError

View File

@@ -309,7 +309,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
def __init__(self, config_entry: ConfigEntry) -> None:
"""Set up the options flow."""
# pylint: disable=hass-component-root-import
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,
)
@@ -451,7 +450,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Configure the Silicon Labs Multiprotocol add-on."""
# pylint: disable=hass-component-root-import
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,
@@ -743,7 +741,6 @@ class OptionsFlowHandler(OptionsFlow, ABC):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Perform initial backup and reconfigure ZHA."""
# pylint: disable=hass-component-root-import
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
ZhaMultiPANMigrationHelper,

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.hardware import HardwareInfo, USBInfo
from homeassistant.components.hardware.models import HardwareInfo, USBInfo
from homeassistant.core import HomeAssistant, callback
from .config_flow import HomeAssistantSkyConnectConfigFlow

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.hardware import BoardInfo, HardwareInfo
from homeassistant.components.hardware.models import BoardInfo, HardwareInfo
from homeassistant.components.hassio import get_os_info
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError

View File

@@ -3,7 +3,6 @@
import logging
from pyHomee import Homee, HomeeAuthFailedException, HomeeConnectionFailedException
from pyHomee.model import HomeeNode
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
@@ -89,40 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
sw_version=homee.settings.version,
)
# Remove devices that are no longer present in homee.
devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id)
for device in devices:
# Check if the device is still present in homee
device_identifiers = {identifier[1] for identifier in device.identifiers}
# homee itself uses just the uid, nodes use uid-nodeid
is_homee_hub = homee.settings.uid in device_identifiers
is_node_present = any(
f"{homee.settings.uid}-{node.id}" in device_identifiers
for node in homee.nodes
)
if not is_node_present and not is_homee_hub:
_LOGGER.info("Removing device %s", device.name)
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=entry.entry_id,
)
# Remove device at runtime when node is removed in homee
async def _remove_node_callback(node: HomeeNode, add: bool) -> None:
"""Call when a node is removed."""
if not add:
device = device_registry.async_get_device(
identifiers={(DOMAIN, f"{entry.runtime_data.settings.uid}-{node.id}")}
)
if device:
_LOGGER.info("Removing device %s", device.name)
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=entry.entry_id,
)
homee.add_nodes_listener(_remove_node_callback)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -63,7 +63,7 @@ rules:
icon-translations: done
reconfiguration-flow: done
repair-issues: todo
stale-devices: done
stale-devices: todo
# Platinum
async-dependency: todo

View File

@@ -27,7 +27,7 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.device_automation.trigger import ( # pylint: disable=hass-component-root-import
from homeassistant.components.device_automation.trigger import (
async_validate_trigger_config,
)
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN, EventDeviceClass

View File

@@ -112,9 +112,6 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
errors=errors or {},
description_placeholders={
"sample_ip": "http://192.168.X.1",
},
)
async def _async_show_reauth_form(
@@ -135,9 +132,6 @@ class HuaweiLteConfigFlow(ConfigFlow, domain=DOMAIN):
}
),
errors=errors or {},
description_placeholders={
"sample_ip": "http://192.168.X.1",
},
)
async def _connect(
@@ -412,10 +406,4 @@ class HuaweiLteOptionsFlow(OptionsFlow):
): bool,
}
)
return self.async_show_form(
step_id="init",
data_schema=data_schema,
description_placeholders={
"sample_ip": "http://192.168.X.1",
},
)
return self.async_show_form(step_id="init", data_schema=data_schema)

View File

@@ -41,7 +41,7 @@
},
"data_description": {
"password": "Password for accessing the router's API. Typically, the same as the one used for the router's web interface.",
"url": "Base URL to the API of the router. Typically, something like `{sample_ip}`. This is the beginning of the location shown in a browser when accessing the router's web interface.",
"url": "Base URL to the API of the router. Typically, something like `http://192.168.X.1`. This is the beginning of the location shown in a browser when accessing the router's web interface.",
"username": "Username for accessing the router's API. Typically, the same as the one used for the router's web interface. Usually, either `admin`, or left empty (recommended if that works).",
"verify_ssl": "Whether to verify the SSL certificate of the router when accessing it. Applicable only if the router is accessed via HTTPS."
},

View File

@@ -68,7 +68,6 @@
"initial_press": "\"{subtype}\" pressed initially",
"repeat": "\"{subtype}\" held down",
"short_release": "\"{subtype}\" released after short press",
"long_press": "\"{subtype}\" long pressed",
"long_release": "[%key:component::hue::device_automation::trigger_type::remote_button_long_release%]",
"double_short_release": "[%key:component::hue::device_automation::trigger_type::remote_double_button_short_press%]",
"start": "[%key:component::hue::device_automation::trigger_type::initial_press%]"

View File

@@ -5,7 +5,6 @@
"config_flow": true,
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",

View File

@@ -125,8 +125,9 @@ class IcloudAccount:
return
try:
api_devices = self.api.devices
# Gets device owners infos
user_info = self.api.devices.user_info
user_info = api_devices.response["userInfo"]
except (
PyiCloudServiceNotActivatedException,
PyiCloudNoDevicesException,

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/icloud",
"iot_class": "cloud_polling",
"loggers": ["keyrings.alt", "pyicloud"],
"requirements": ["pyicloud==2.1.0"]
"requirements": ["pyicloud==2.0.3"]
}

View File

@@ -13,7 +13,7 @@ from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import API_ACCESS_URL, DOMAIN
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -57,8 +57,5 @@ class IgloohomeConfigFlow(ConfigFlow, domain=DOMAIN):
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
description_placeholders={"api_access_url": API_ACCESS_URL},
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

View File

@@ -1,4 +1,3 @@
"""Constants for the igloohome integration."""
DOMAIN = "igloohome"
API_ACCESS_URL = "https://access.igloocompany.co/api-access"

View File

@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"description": "Copy & paste your [API access credentials]({api_access_url}) to give Home Assistant access to your account.",
"description": "Copy & paste your [API access credentials](https://access.igloocompany.co/api-access) to give Home Assistant access to your account.",
"data": {
"client_id": "Client ID",
"client_secret": "Client secret"

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==12.0.0"]
"requirements": ["Pillow==11.3.0"]
}

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