Compare commits

...

67 Commits

Author SHA1 Message Date
Paulus Schoutsen
3499ed7a98 2025.9.3 (#152237) 2025-09-13 08:44:08 -04:00
Paulus Schoutsen
2c809d5903 Bump version to 2025.9.3 2025-09-13 12:14:15 +00:00
J. Nick Koston
40988198f3 Bump habluetooth to 5.6.4 (#152227) 2025-09-13 12:14:08 +00:00
Franck Nijhof
ab5d1d27f1 2025.9.2 (#152198) 2025-09-12 23:10:27 +02:00
wollew
1c10b85fed Use position percentage for closed status in Velux (#151679)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:39:06 +00:00
Franck Nijhof
91a7db08ff Bump version to 2025.9.2 2025-09-12 20:20:56 +00:00
Bram Kragten
a764d54123 Update frontend to 20250903.5 (#152170) 2025-09-12 20:20:44 +00:00
Joakim Sørensen
dc09e33556 Bump hass-nabucasa from 1.1.0 to 1.1.1 (#152147) 2025-09-12 20:20:43 +00:00
Simone Chemelli
14173bd9ec Fix reauth for Alexa Devices (#152128) 2025-09-12 20:20:42 +00:00
Jan Bouwhuis
d2e7537629 Fix supported _color_modes attribute not set for on/off MQTT JSON light (#152126) 2025-09-12 20:20:41 +00:00
J. Nick Koston
9a165a64fe Fix DoorBird being updated with wrong IP addresses during discovery (#152088)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-12 20:20:39 +00:00
Abílio Costa
9c749a6abc Fix duplicated IP port usage in Govee Light Local (#152087) 2025-09-12 20:20:38 +00:00
J. Nick Koston
2e33222c71 Fix HomeKit Controller stale values at startup (#152086)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-09-12 20:20:37 +00:00
Maciej Bieniek
ab1c2c4f70 Bump accuweather to version 4.2.1 (#152029) 2025-09-12 20:20:36 +00:00
Joost Lekkerkerker
529219ae69 Bump yt-dlp to 2025.09.05 (#152006) 2025-09-12 20:20:35 +00:00
J. Nick Koston
d6ce71fa61 Bump habluetooth to 5.6.2 (#151985) 2025-09-12 20:20:33 +00:00
peteS-UK
e5b67d513a Fix playlist media_class_filter in search_media for squeezebox (#151973) 2025-09-12 20:20:32 +00:00
peteS-UK
a547179f66 Fix for squeezebox track content_type (#151963) 2025-09-12 20:20:31 +00:00
epenet
8c61788a7d Fix invalid logger in Tuya (#151957) 2025-09-12 20:19:26 +00:00
J. Nick Koston
6b934d94db Bump habluetooth to 5.6.0 (#151942) 2025-09-12 20:18:38 +00:00
J. Nick Koston
d30ad82774 Bump bleak-esphome to 3.3.0 (#151922)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-12 20:18:37 +00:00
J. Nick Koston
4618b33e93 Bump habluetooth to 5.5.1 (#151921) 2025-09-12 20:18:36 +00:00
epenet
d6299094db Fix _is_valid_suggested_unit in sensor platform (#151912) 2025-09-12 20:18:35 +00:00
Simone Chemelli
087d9d30c0 Avoid cleanup/recreate of device_trackers not linked to a device for Vodafone Station (#151904) 2025-09-12 20:18:33 +00:00
Simone Chemelli
f07890cf5c Bump aiovodafone to 1.2.1 (#151901) 2025-09-12 20:18:31 +00:00
Avi Miller
e5b78cc481 Bump aiolifx-themes to 1.0.2 to support newer LIFX devices (#151898)
Signed-off-by: Avi Miller <me@dje.li>
2025-09-12 20:18:30 +00:00
Manu
12b409d8e1 Bump aiontfy to v0.5.5 (#151869) 2025-09-12 20:18:29 +00:00
Maciej Bieniek
def5408db8 Use native_visibility property instead of visibility for OpenWeatherMap weather entity (#151867) 2025-09-12 20:18:28 +00:00
Robert Resch
f105b45ee2 Bump aioecowitt to 2025.9.1 (#151859) 2025-09-12 20:18:26 +00:00
wollew
9d904c30a7 fix rain sensor for Velux GPU windows (#151857) 2025-09-12 20:18:25 +00:00
Martins Sipenko
99b047939f Update pysmarty2 to 0.10.3 (#151855) 2025-09-12 20:18:24 +00:00
J. Nick Koston
3a615908ee Bump aioharmony to 0.5.3 (#151853) 2025-09-12 20:18:23 +00:00
David Knowles
baff541f46 Bump pydrawise to 2025.9.0 (#151842) 2025-09-12 20:18:22 +00:00
jan iversen
6d8c35cfe9 removed assert fron entity in modbus. (#151834) 2025-09-12 20:18:20 +00:00
jan iversen
b8d9883e74 max_temp / min_temp in modbus light could only be int, otherwise an assert was provoked. (#151833) 2025-09-12 20:18:19 +00:00
jan iversen
c3c65af450 Allow delay > 1 in modbus. (#151832) 2025-09-12 20:18:18 +00:00
Maciej Bieniek
3af8616764 Mark Tractive switches as unavailable when tacker is in the enegy saving zone (#151817) 2025-09-12 20:18:17 +00:00
Matthias Alphart
64ec4609c5 Fix KNX Light - individual color initialisation from UI config (#151815) 2025-09-12 20:18:15 +00:00
Matthias Alphart
c78bc26b83 Fix KNX BinarySensor config_store data (#151808) 2025-09-12 20:18:14 +00:00
J. Nick Koston
0c093646c9 Bump habluetooth to 5.3.1 (#151803) 2025-09-12 20:18:13 +00:00
Simone Chemelli
1b27acdde0 Improve config entry migration for edge cases in Alexa Devices (#151788) 2025-09-12 20:18:12 +00:00
Ludovic BOUÉ
9dafc0e02f Remove device class for Matter NitrogenDioxideSensor (#151782)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-09-12 20:18:10 +00:00
Tsvi Mostovicz
0091dafcb0 Revert "Jewish Calendar add coordinator " (#151780) 2025-09-12 20:18:09 +00:00
Jan Bouwhuis
b387acffb7 Fix update of the entity ID does not clean up an old restored state (#151696)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-09-12 20:18:08 +00:00
blotus
36b3133fa2 Fix support for Ecowitt soil moisture sensors (#151685) 2025-09-12 20:18:06 +00:00
hbludworth
fe01e96012 Fix Aladdin Connect state not updating (#151652)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-12 20:18:05 +00:00
Thomas55555
0b56ec16ed Add event entity on websocket ready in Husqvarna Automower (#151428) 2025-09-12 20:18:04 +00:00
Mark Adkins
ca79f4c963 Update SharkIQ authentication method (#151046) 2025-09-12 20:18:02 +00:00
Bob Igo
9a43f2776d Fix XMPP not working with non-TLS servers (#150957) 2025-09-12 20:18:01 +00:00
Franck Nijhof
0cda883b56 2025.9.1 (#151766) 2025-09-05 13:13:34 +02:00
Franck Nijhof
ae58e633f0 Bump version to 2025.9.1 2025-09-05 10:33:36 +00:00
jan iversen
06480bfd9d Fix enable/disable entity in modbus (#151626) 2025-09-05 10:33:04 +00:00
Artur Pragacz
625f586945 Fix recognition of entity names in default agent with interpunction (#151759) 2025-09-05 10:30:27 +00:00
Richard Kroegel
7dbeaa475d Bump bimmer_connected to 0.17.3 (#151756) 2025-09-05 10:30:24 +00:00
David Knowles
dff3d5f8af Bump pyschlage to 2025.9.0 (#151731) 2025-09-05 10:30:21 +00:00
Michael Hansen
89c335919a Handle match failures in intent HTTP API (#151726) 2025-09-05 10:30:16 +00:00
Daniel Hjelseth Høyer
2bb4573357 Update Mill library 0.13.1 (#151712) 2025-09-05 10:30:01 +00:00
Dan Raper
7037ce989c Bump ohmepy version to 1.5.2 (#151707) 2025-09-05 10:29:58 +00:00
Dan Raper
bfdd2053ba Require OhmeAdvancedSettingsCoordinator to run regardless of entities (#151701) 2025-09-05 10:29:55 +00:00
Bram Kragten
fcc3f92f8c Update frontend to 20250903.3 (#151694) 2025-09-05 10:29:51 +00:00
Marcel van der Veldt
8710267d53 Bump aiohue to 4.7.5 (#151684) 2025-09-05 10:29:48 +00:00
Imeon-Energy
85b6adcc9a Fix, entities stay unavailable after timeout error, Imeon inverter integration (#151671)
Co-authored-by: TheBushBoy <theodavid@icloud.com>
2025-09-05 10:29:46 +00:00
Felipe Santos
beec6e86e0 Fix WebSocket proxy for add-ons not forwarding ping/pong frame data (#151654) 2025-09-05 10:29:45 +00:00
Pete Sage
3dacffaaf9 Fix Sonos Dialog Select type conversion (#151649) 2025-09-05 10:29:41 +00:00
Manu
d90f2a1de1 Correct capitalization of "FRITZ!Box" in FRITZ!Box Tools integration (#151637) 2025-09-05 10:29:39 +00:00
karwosts
b6c9217429 Add missing device trigger duration localizations (#151578)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-09-05 10:29:37 +00:00
Marcel van der Veldt
7fc8da6769 Add support for migrated Hue bridge (#151411)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-09-05 10:29:34 +00:00
120 changed files with 1850 additions and 550 deletions

4
CODEOWNERS generated
View File

@@ -1690,8 +1690,8 @@ build.json @home-assistant/supervisor
/tests/components/vegehub/ @ghowevege
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio
/homeassistant/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/tests/components/velux/ @Julius2342 @DeerMaximum @pawlizio @wollew
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.0"],
"requirements": ["accuweather==4.2.1"],
"single_config_entry": true
}

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from genie_partner_sdk.client import AladdinConnectClient
from genie_partner_sdk.model import GarageDoor
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
@@ -36,22 +35,7 @@ async def async_setup_entry(
api.AsyncConfigEntryAuth(aiohttp_client.async_get_clientsession(hass), session)
)
sdk_doors = await client.get_doors()
# Convert SDK GarageDoor objects to integration GarageDoor objects
doors = [
GarageDoor(
{
"device_id": door.device_id,
"door_number": door.door_number,
"name": door.name,
"status": door.status,
"link_status": door.link_status,
"battery_level": door.battery_level,
}
)
for door in sdk_doors
]
doors = await client.get_doors()
entry.runtime_data = {
door.unique_id: AladdinConnectCoordinator(hass, entry, client, door)

View File

@@ -41,4 +41,10 @@ class AladdinConnectCoordinator(DataUpdateCoordinator[GarageDoor]):
async def _async_update_data(self) -> GarageDoor:
"""Fetch data from the Aladdin Connect API."""
await self.client.update_door(self.data.device_id, self.data.door_number)
self.data.status = self.client.get_door_status(
self.data.device_id, self.data.door_number
)
self.data.battery_level = self.client.get_battery_status(
self.data.device_id, self.data.door_number
)
return self.data

View File

@@ -49,7 +49,9 @@ class AladdinCoverEntity(AladdinConnectEntity, CoverEntity):
@property
def is_closed(self) -> bool | None:
"""Update is closed attribute."""
return self.coordinator.data.status == "closed"
if (status := self.coordinator.data.status) is None:
return None
return status == "closed"
@property
def is_closing(self) -> bool | None:

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/aladdin_connect",
"integration_type": "hub",
"iot_class": "cloud_polling",
"requirements": ["genie-partner-sdk==1.0.10"]
"requirements": ["genie-partner-sdk==1.0.11"]
}

View File

@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import _LOGGER, CONF_LOGIN_DATA, COUNTRY_DOMAINS, DOMAIN
from .const import _LOGGER, CONF_LOGIN_DATA, CONF_SITE, COUNTRY_DOMAINS, DOMAIN
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .services import async_setup_services
@@ -42,7 +42,23 @@ async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bo
async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version == 1 and entry.minor_version == 1:
if entry.version == 1 and entry.minor_version < 3:
if CONF_SITE in entry.data:
# Site in data (wrong place), just move to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA][CONF_SITE] = new_data[CONF_SITE]
new_data.pop(CONF_SITE)
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=3
)
return True
if CONF_SITE in entry.data[CONF_LOGIN_DATA]:
# Site is there, just update version to avoid future migrations
hass.config_entries.async_update_entry(entry, version=1, minor_version=3)
return True
_LOGGER.debug(
"Migrating from version %s.%s", entry.version, entry.minor_version
)
@@ -53,10 +69,10 @@ async def async_migrate_entry(hass: HomeAssistant, entry: AmazonConfigEntry) ->
# Add site to login data
new_data = entry.data.copy()
new_data[CONF_LOGIN_DATA]["site"] = f"https://www.amazon.{domain}"
new_data[CONF_LOGIN_DATA][CONF_SITE] = f"https://www.amazon.{domain}"
hass.config_entries.async_update_entry(
entry, data=new_data, version=1, minor_version=2
entry, data=new_data, version=1, minor_version=3
)
_LOGGER.info(

View File

@@ -52,7 +52,7 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Alexa Devices."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -107,7 +107,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
try:
await validate_input(self.hass, {**reauth_entry.data, **user_input})
data = await validate_input(
self.hass, {**reauth_entry.data, **user_input}
)
except CannotConnect:
errors["base"] = "cannot_connect"
except (CannotAuthenticate, TypeError):
@@ -119,8 +121,9 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry,
data={
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_CODE: user_input[CONF_CODE],
CONF_LOGIN_DATA: data,
},
)

View File

@@ -6,6 +6,7 @@ _LOGGER = logging.getLogger(__package__)
DOMAIN = "alexa_devices"
CONF_LOGIN_DATA = "login_data"
CONF_SITE = "site"
DEFAULT_DOMAIN = "com"
COUNTRY_DOMAINS = {

View File

@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.5.2",
"bluetooth-data-tools==1.28.2",
"dbus-fast==2.44.3",
"habluetooth==5.3.0"
"habluetooth==5.6.4"
]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.17.2"]
"requirements": ["bimmer-connected[china]==0.17.3"]
}

View File

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

View File

@@ -35,7 +35,7 @@ from hassil.recognize import (
)
from hassil.string_matcher import UnmatchedRangeEntity, UnmatchedTextEntity
from hassil.trie import Trie
from hassil.util import merge_dict
from hassil.util import merge_dict, remove_punctuation
from home_assistant_intents import (
ErrorKey,
FuzzyConfig,
@@ -327,12 +327,10 @@ class DefaultAgent(ConversationEntity):
if self._exposed_names_trie is not None:
# Filter by input string
text_lower = user_input.text.strip().lower()
text = remove_punctuation(user_input.text).strip().lower()
slot_lists["name"] = TextSlotList(
name="name",
values=[
result[2] for result in self._exposed_names_trie.find(text_lower)
],
values=[result[2] for result in self._exposed_names_trie.find(text)],
)
start = time.monotonic()
@@ -1263,7 +1261,7 @@ class DefaultAgent(ConversationEntity):
name_list = TextSlotList.from_tuples(exposed_entity_names, allow_template=False)
for name_value in name_list.values:
assert isinstance(name_value.text_in, TextChunk)
name_text = name_value.text_in.text.strip().lower()
name_text = remove_punctuation(name_value.text_in.text).strip().lower()
self._exposed_names_trie.insert(name_text, name_value)
self._slot_lists = {

View File

@@ -19,8 +19,10 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from homeassistant.helpers.typing import VolDictType
@@ -103,6 +105,43 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
"""Initialize the DoorBird config flow."""
self.discovery_schema: vol.Schema | None = None
async def _async_verify_existing_device_for_discovery(
self,
existing_entry: ConfigEntry,
host: str,
macaddress: str,
) -> None:
"""Verify discovered device matches existing entry before updating IP.
This method performs the following verification steps:
1. Ensures that the stored credentials work before updating the entry.
2. Verifies that the device at the discovered IP address has the expected MAC address.
"""
info, errors = await self._async_validate_or_error(
{
**existing_entry.data,
CONF_HOST: host,
}
)
if errors:
_LOGGER.debug(
"Cannot validate DoorBird at %s with existing credentials: %s",
host,
errors,
)
raise AbortFlow("cannot_connect")
# Verify the MAC address matches what was advertised
if format_mac(info["mac_addr"]) != format_mac(macaddress):
_LOGGER.debug(
"DoorBird at %s reports MAC %s but zeroconf advertised %s, ignoring",
host,
info["mac_addr"],
macaddress,
)
raise AbortFlow("wrong_device")
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -172,7 +211,22 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(macaddress)
host = discovery_info.host
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
# Check if we have an existing entry for this MAC
existing_entry = self.hass.config_entries.async_entry_for_domain_unique_id(
DOMAIN, macaddress
)
if existing_entry:
# Check if the host is actually changing
if existing_entry.data.get(CONF_HOST) != host:
await self._async_verify_existing_device_for_discovery(
existing_entry, host, macaddress
)
# All checks passed or no change needed, abort
# if already configured with potential IP update
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self._async_abort_entries_match({CONF_HOST: host})

View File

@@ -49,6 +49,8 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"link_local_address": "Link local addresses are not supported",
"not_doorbird_device": "This device is not a DoorBird",
"not_ipv4_address": "Only IPv4 addresses are supported",
"wrong_device": "Device MAC address does not match",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"flow_title": "{name} ({host})",

View File

@@ -6,5 +6,5 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"iot_class": "local_push",
"requirements": ["aioecowitt==2025.9.0"]
"requirements": ["aioecowitt==2025.9.1"]
}

View File

@@ -218,6 +218,12 @@ ECOWITT_SENSORS_MAPPING: Final = {
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
EcoWittSensorTypes.SOIL_MOISTURE: SensorEntityDescription(
key="SOIL_MOISTURE",
device_class=SensorDeviceClass.MOISTURE,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
}

View File

@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.2.0"]
"requirements": ["eq3btsmart==2.1.0", "bleak-esphome==3.3.0"]
}

View File

@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==39.0.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.2.0"
"bleak-esphome==3.3.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}

View File

@@ -14,6 +14,9 @@
"toggle": "[%key:common::device_automation::action_type::toggle%]",
"turn_on": "[%key:common::device_automation::action_type::turn_on%]",
"turn_off": "[%key:common::device_automation::action_type::turn_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -183,8 +183,8 @@
"description": "Sets a new password for the guest Wi-Fi. The password must be between 8 and 63 characters long. If no additional parameter is set, the password will be auto-generated with a length of 12 characters.",
"fields": {
"device_id": {
"name": "Fritz!Box Device",
"description": "Select the Fritz!Box to configure."
"name": "FRITZ!Box Device",
"description": "Select the FRITZ!Box to configure."
},
"password": {
"name": "[%key:common::config_flow::data::password%]",

View File

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

View File

@@ -26,16 +26,11 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
"""Set up Govee light local from a config entry."""
# Get source IPs for all enabled adapters
source_ips = await network.async_get_enabled_source_ips(hass)
source_ips = await async_get_source_ips(hass)
_LOGGER.debug("Enabled source IPs: %s", source_ips)
coordinator: GoveeLocalApiCoordinator = GoveeLocalApiCoordinator(
hass=hass,
config_entry=entry,
source_ips=[
source_ip for source_ip in source_ips if isinstance(source_ip, IPv4Address)
],
hass=hass, config_entry=entry, source_ips=source_ips
)
async def await_cleanup():
@@ -76,3 +71,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -
async def async_unload_entry(hass: HomeAssistant, entry: GoveeLocalConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_get_source_ips(
hass: HomeAssistant,
) -> set[str]:
"""Get the source ips for Govee local."""
source_ips = await network.async_get_enabled_source_ips(hass)
return {
str(source_ip) for source_ip in source_ips if isinstance(source_ip, IPv4Address)
}

View File

@@ -4,15 +4,14 @@ from __future__ import annotations
import asyncio
from contextlib import suppress
from ipaddress import IPv4Address
import logging
from govee_local_api import GoveeController
from homeassistant.components import network
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_flow
from . import async_get_source_ips
from .const import (
CONF_LISTENING_PORT_DEFAULT,
CONF_MULTICAST_ADDRESS_DEFAULT,
@@ -24,11 +23,11 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool:
async def _async_discover(hass: HomeAssistant, adapter_ip: str) -> bool:
controller: GoveeController = GoveeController(
loop=hass.loop,
logger=_LOGGER,
listening_address=str(adapter_ip),
listening_address=adapter_ip,
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
broadcast_port=CONF_TARGET_PORT_DEFAULT,
listening_port=CONF_LISTENING_PORT_DEFAULT,
@@ -62,14 +61,8 @@ async def _async_discover(hass: HomeAssistant, adapter_ip: IPv4Address) -> bool:
async def _async_has_devices(hass: HomeAssistant) -> bool:
"""Return if there are devices that can be discovered."""
# Get source IPs for all enabled adapters
source_ips = await network.async_get_enabled_source_ips(hass)
_LOGGER.debug("Enabled source IPs: %s", source_ips)
# Run discovery on every IPv4 address and gather results
results = await asyncio.gather(
*[_async_discover(hass, ip) for ip in source_ips if isinstance(ip, IPv4Address)]
)
source_ips = await async_get_source_ips(hass)
results = await asyncio.gather(*[_async_discover(hass, ip) for ip in source_ips])
return any(results)

View File

@@ -2,7 +2,6 @@
import asyncio
from collections.abc import Callable
from ipaddress import IPv4Address
import logging
from govee_local_api import GoveeController, GoveeDevice
@@ -30,7 +29,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
self,
hass: HomeAssistant,
config_entry: GoveeLocalConfigEntry,
source_ips: list[IPv4Address],
source_ips: set[str],
) -> None:
"""Initialize my coordinator."""
super().__init__(
@@ -45,7 +44,7 @@ class GoveeLocalApiCoordinator(DataUpdateCoordinator[list[GoveeDevice]]):
GoveeController(
loop=hass.loop,
logger=_LOGGER,
listening_address=str(source_ip),
listening_address=source_ip,
broadcast_address=CONF_MULTICAST_ADDRESS_DEFAULT,
broadcast_port=CONF_TARGET_PORT_DEFAULT,
listening_port=CONF_LISTENING_PORT_DEFAULT,

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.2"],
"requirements": ["aioharmony==0.5.3"],
"ssdp": [
{
"manufacturer": "Logitech",

View File

@@ -303,9 +303,9 @@ async def _websocket_forward(
elif msg.type is aiohttp.WSMsgType.BINARY:
await ws_to.send_bytes(msg.data)
elif msg.type is aiohttp.WSMsgType.PING:
await ws_to.ping()
await ws_to.ping(msg.data)
elif msg.type is aiohttp.WSMsgType.PONG:
await ws_to.pong()
await ws_to.pong(msg.data)
elif ws_to.closed:
await ws_to.close(code=ws_to.close_code, message=msg.extra) # type: ignore[arg-type]
except RuntimeError:

View File

@@ -20,7 +20,12 @@ from aiohomekit.exceptions import (
EncryptionError,
)
from aiohomekit.model import Accessories, Accessory, Transport
from aiohomekit.model.characteristics import Characteristic, CharacteristicsTypes
from aiohomekit.model.characteristics import (
EVENT_CHARACTERISTICS,
Characteristic,
CharacteristicPermissions,
CharacteristicsTypes,
)
from aiohomekit.model.services import Service, ServicesTypes
from homeassistant.components.thread import async_get_preferred_dataset
@@ -179,6 +184,21 @@ class HKDevice:
for aid_iid in characteristics:
self.pollable_characteristics.discard(aid_iid)
def get_all_pollable_characteristics(self) -> set[tuple[int, int]]:
"""Get all characteristics that can be polled.
This is used during startup to poll all readable characteristics
before entities have registered what they care about.
"""
return {
(accessory.aid, char.iid)
for accessory in self.entity_map.accessories
for service in accessory.services
for char in service.characteristics
if CharacteristicPermissions.paired_read in char.perms
and char.type not in EVENT_CHARACTERISTICS
}
def add_watchable_characteristics(
self, characteristics: list[tuple[int, int]]
) -> None:
@@ -309,9 +329,13 @@ class HKDevice:
await self.async_process_entity_map()
if transport != Transport.BLE:
# Do a single poll to make sure the chars are
# up to date so we don't restore old data.
await self.async_update()
# When Home Assistant starts, we restore the accessory map from storage
# which contains characteristic values from when HA was last running.
# These values are stale and may be incorrect (e.g., Ecobee thermostats
# report 100°C when restarting). We need to poll for fresh values before
# creating entities. Use poll_all=True since entities haven't registered
# their characteristics yet.
await self.async_update(poll_all=True)
self._async_start_polling()
# If everything is up to date, we can create the entities
@@ -863,9 +887,25 @@ class HKDevice:
"""Request an debounced update from the accessory."""
await self._debounced_update.async_call()
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
async def async_update(
self, now: datetime | None = None, *, poll_all: bool = False
) -> None:
"""Poll state of all entities attached to this bridge/accessory.
Args:
now: The current time (used by time interval callbacks).
poll_all: If True, poll all readable characteristics instead
of just the registered ones.
This is useful during initial setup before entities have
registered their characteristics.
"""
if poll_all:
# Poll all readable characteristics during initial startup
# excluding device trigger characteristics (buttons, doorbell, etc.)
to_poll = self.get_all_pollable_characteristics()
else:
to_poll = self.pollable_characteristics
if not to_poll:
self.async_update_available_state()
_LOGGER.debug(

View File

@@ -9,7 +9,9 @@ from typing import Any
import aiohttp
from aiohue import LinkButtonNotPressed, create_app_key
from aiohue.discovery import DiscoveredHueBridge, discover_bridge, discover_nupnp
from aiohue.errors import AiohueException
from aiohue.util import normalize_bridge_id
from aiohue.v2 import HueBridgeV2
import slugify as unicode_slug
import voluptuous as vol
@@ -40,6 +42,9 @@ HUE_MANUFACTURERURL = ("http://www.philips.com", "http://www.philips-hue.com")
HUE_IGNORED_BRIDGE_NAMES = ["Home Assistant Bridge", "Espalexa"]
HUE_MANUAL_BRIDGE_ID = "manual"
BSB002_MODEL_ID = "BSB002"
BSB003_MODEL_ID = "BSB003"
class HueFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Hue config flow."""
@@ -74,7 +79,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
"""Return a DiscoveredHueBridge object."""
try:
bridge = await discover_bridge(
host, websession=aiohttp_client.async_get_clientsession(self.hass)
host,
websession=aiohttp_client.async_get_clientsession(
# NOTE: we disable SSL verification for now due to the fact that the (BSB003)
# Hue bridge uses a certificate from a on-bridge root authority.
# We need to specifically handle this case in a follow-up update.
self.hass,
verify_ssl=False,
),
)
except aiohttp.ClientError as err:
LOGGER.warning(
@@ -110,7 +122,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
try:
async with asyncio.timeout(5):
bridges = await discover_nupnp(
websession=aiohttp_client.async_get_clientsession(self.hass)
websession=aiohttp_client.async_get_clientsession(
self.hass, verify_ssl=False
)
)
except TimeoutError:
bridges = []
@@ -178,7 +192,9 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
app_key = await create_app_key(
bridge.host,
f"home-assistant#{device_name}",
websession=aiohttp_client.async_get_clientsession(self.hass),
websession=aiohttp_client.async_get_clientsession(
self.hass, verify_ssl=False
),
)
except LinkButtonNotPressed:
errors["base"] = "register_failed"
@@ -228,7 +244,6 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured(
updates={CONF_HOST: discovery_info.host}, reload_on_update=True
)
# we need to query the other capabilities too
bridge = await self._get_bridge(
discovery_info.host, discovery_info.properties["bridgeid"]
@@ -236,6 +251,14 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
if bridge is None:
return self.async_abort(reason="cannot_connect")
self.bridge = bridge
if (
bridge.supports_v2
and discovery_info.properties.get("modelid") == BSB003_MODEL_ID
):
# try to handle migration of BSB002 --> BSB003
if await self._check_migrated_bridge(bridge):
return self.async_abort(reason="migrated_bridge")
return await self.async_step_link()
async def async_step_homekit(
@@ -272,6 +295,55 @@ class HueFlowHandler(ConfigFlow, domain=DOMAIN):
self.bridge = bridge
return await self.async_step_link()
async def _check_migrated_bridge(self, bridge: DiscoveredHueBridge) -> bool:
"""Check if the discovered bridge is a migrated bridge."""
# Try to handle migration of BSB002 --> BSB003.
# Once we detect a BSB003 bridge on the network which has not yet been
# configured in HA (otherwise we would have had a unique id match),
# we check if we have any existing (BSB002) entries and if we can connect to the
# new bridge with our previously stored api key.
# If that succeeds, we migrate the entry to the new bridge.
for conf_entry in self.hass.config_entries.async_entries(
DOMAIN, include_ignore=False, include_disabled=False
):
if conf_entry.data[CONF_API_VERSION] != 2:
continue
if conf_entry.data[CONF_HOST] == bridge.host:
continue
# found an existing (BSB002) bridge entry,
# check if we can connect to the new BSB003 bridge using the old credentials
api = HueBridgeV2(bridge.host, conf_entry.data[CONF_API_KEY])
try:
await api.fetch_full_state()
except (AiohueException, aiohttp.ClientError):
continue
old_bridge_id = conf_entry.unique_id
assert old_bridge_id is not None
# found a matching entry, migrate it
self.hass.config_entries.async_update_entry(
conf_entry,
data={
**conf_entry.data,
CONF_HOST: bridge.host,
},
unique_id=bridge.id,
)
# also update the bridge device
dev_reg = dr.async_get(self.hass)
if bridge_device := dev_reg.async_get_device(
identifiers={(DOMAIN, old_bridge_id)}
):
dev_reg.async_update_device(
bridge_device.id,
# overwrite identifiers with new bridge id
new_identifiers={(DOMAIN, bridge.id)},
# overwrite mac addresses with empty set to drop the old (incorrect) addresses
# this will be auto corrected once the integration is loaded
new_connections=set(),
)
return True
return False
class HueV1OptionsFlowHandler(OptionsFlow):
"""Handle Hue options for V1 implementation."""

View File

@@ -10,6 +10,6 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["aiohue"],
"requirements": ["aiohue==4.7.4"],
"requirements": ["aiohue==4.7.5"],
"zeroconf": ["_hue._tcp.local."]
}

View File

@@ -36,12 +36,13 @@ async def async_setup_entry(
"""Set up Automower message event entities.
Entities are created dynamically based on messages received from the API,
but only for mowers that support message events.
but only for mowers that support message events after the WebSocket connection
is ready.
"""
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
restored_mowers = {
restored_mowers: set[str] = {
entry.unique_id.removesuffix("_message")
for entry in er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
@@ -49,14 +50,20 @@ async def async_setup_entry(
if entry.domain == EVENT_DOMAIN
}
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
@callback
def _on_ws_ready() -> None:
async_add_entities(
AutomowerMessageEventEntity(mower_id, coordinator, websocket_alive=True)
for mower_id in restored_mowers
if mower_id in coordinator.data
)
coordinator.api.unregister_ws_ready_callback(_on_ws_ready)
coordinator.api.register_ws_ready_callback(_on_ws_ready)
@callback
def _handle_message(msg: SingleMessageData) -> None:
"""Add entity dynamically if a new mower sends messages."""
if msg.id in restored_mowers:
return
@@ -78,11 +85,17 @@ class AutomowerMessageEventEntity(AutomowerBaseEntity, EventEntity):
self,
mower_id: str,
coordinator: AutomowerDataUpdateCoordinator,
*,
websocket_alive: bool | None = None,
) -> None:
"""Initialize Automower message event entity."""
super().__init__(mower_id, coordinator)
self._attr_unique_id = f"{mower_id}_message"
self.websocket_alive: bool = coordinator.websocket_alive
self.websocket_alive: bool = (
websocket_alive
if websocket_alive is not None
else coordinator.websocket_alive
)
@property
def available(self) -> bool:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/hydrawise",
"iot_class": "cloud_polling",
"loggers": ["pydrawise"],
"requirements": ["pydrawise==2025.7.0"]
"requirements": ["pydrawise==2025.9.0"]
}

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["imeon_inverter_api==0.3.14"],
"requirements": ["imeon_inverter_api==0.3.16"],
"ssdp": [
{
"manufacturer": "IMEON",

View File

@@ -615,7 +615,7 @@ class IntentHandleView(http.HomeAssistantView):
intent_result = await intent.async_handle(
hass, DOMAIN, intent_name, slots, "", self.context(request)
)
except intent.IntentHandleError as err:
except (intent.IntentHandleError, intent.MatchFailedError) as err:
intent_result = intent.IntentResponse(language=language)
intent_result.async_set_speech(str(err))

View File

@@ -29,8 +29,7 @@ from .const import (
DEFAULT_LANGUAGE,
DOMAIN,
)
from .coordinator import JewishCalendarData, JewishCalendarUpdateCoordinator
from .entity import JewishCalendarConfigEntry
from .entity import JewishCalendarConfigEntry, JewishCalendarData
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -70,7 +69,7 @@ async def async_setup_entry(
)
)
data = JewishCalendarData(
config_entry.runtime_data = JewishCalendarData(
language,
diaspora,
location,
@@ -78,11 +77,8 @@ async def async_setup_entry(
havdalah_offset,
)
coordinator = JewishCalendarUpdateCoordinator(hass, config_entry, data)
await coordinator.async_config_entry_first_refresh()
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
@@ -90,13 +86,7 @@ async def async_unload_entry(
hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
coordinator = config_entry.runtime_data
if coordinator.event_unsub:
coordinator.event_unsub()
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_migrate_entry(

View File

@@ -72,7 +72,8 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return true if sensor is on."""
return self.entity_description.is_on(self.coordinator.zmanim)(dt_util.now())
zmanim = self.make_zmanim(dt.date.today())
return self.entity_description.is_on(zmanim)(dt_util.now())
def _update_times(self, zmanim: Zmanim) -> list[dt.datetime | None]:
"""Return a list of times to update the sensor."""

View File

@@ -1,116 +0,0 @@
"""Data update coordinator for Jewish calendar."""
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import homeassistant.util.dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarUpdateCoordinator]
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
dateinfo: HDateInfo | None = None
zmanim: Zmanim | None = None
class JewishCalendarUpdateCoordinator(DataUpdateCoordinator[JewishCalendarData]):
"""Data update coordinator class for Jewish calendar."""
config_entry: JewishCalendarConfigEntry
event_unsub: CALLBACK_TYPE | None = None
def __init__(
self,
hass: HomeAssistant,
config_entry: JewishCalendarConfigEntry,
data: JewishCalendarData,
) -> None:
"""Initialize the coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, config_entry=config_entry)
self.data = data
self._unsub_update: CALLBACK_TYPE | None = None
set_language(data.language)
async def _async_update_data(self) -> JewishCalendarData:
"""Return HDate and Zmanim for today."""
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date()
self.data.dateinfo = HDateInfo(today, self.data.diaspora)
self.data.zmanim = self.make_zmanim(today)
self.async_schedule_future_update()
return self.data
@callback
def async_schedule_future_update(self) -> None:
"""Schedule the next update of the sensor for the upcoming midnight."""
# Cancel any existing update
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
# Calculate the next midnight
next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1)
_LOGGER.debug("Scheduling next update at %s", next_midnight)
# Schedule update at next midnight
self._unsub_update = event.async_track_point_in_time(
self.hass, self._handle_midnight_update, next_midnight
)
@callback
def _handle_midnight_update(self, _now: dt.datetime) -> None:
"""Handle midnight update callback."""
self._unsub_update = None
self.async_set_updated_data(self.data)
async def async_shutdown(self) -> None:
"""Cancel any scheduled updates when the coordinator is shutting down."""
await super().async_shutdown()
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
@property
def zmanim(self) -> Zmanim:
"""Return the current Zmanim."""
assert self.data.zmanim is not None, "Zmanim data not available"
return self.data.zmanim
@property
def dateinfo(self) -> HDateInfo:
"""Return the current HDateInfo."""
assert self.data.dateinfo is not None, "HDateInfo data not available"
return self.data.dateinfo

View File

@@ -24,5 +24,5 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data.data), TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data), TO_REDACT),
}

View File

@@ -1,22 +1,48 @@
"""Entity representing a Jewish Calendar sensor."""
from abc import abstractmethod
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import Zmanim
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import JewishCalendarConfigEntry, JewishCalendarUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData]
class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
@dataclass
class JewishCalendarDataResults:
"""Jewish Calendar results dataclass."""
dateinfo: HDateInfo
zmanim: Zmanim
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
results: JewishCalendarDataResults | None = None
class JewishCalendarEntity(Entity):
"""An HA implementation for Jewish Calendar entity."""
_attr_has_entity_name = True
@@ -29,13 +55,23 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
description: EntityDescription,
) -> None:
"""Initialize a Jewish Calendar entity."""
super().__init__(config_entry.runtime_data)
self.entity_description = description
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
)
self.data = config_entry.runtime_data
set_language(self.data.language)
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
@@ -56,9 +92,10 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
def _schedule_update(self) -> None:
"""Schedule the next update of the sensor."""
now = dt_util.now()
zmanim = self.make_zmanim(now.date())
update = dt_util.start_of_local_day() + dt.timedelta(days=1)
for update_time in self._update_times(self.coordinator.zmanim):
for update_time in self._update_times(zmanim):
if update_time is not None and now < update_time < update:
update = update_time
@@ -73,4 +110,17 @@ class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
"""Update the sensor data."""
self._update_unsub = None
self._schedule_update()
self.create_results(now)
self.async_write_ha_state()
def create_results(self, now: dt.datetime | None = None) -> None:
"""Create the results for the sensor."""
if now is None:
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date()
zmanim = self.make_zmanim(today)
dateinfo = HDateInfo(today, diaspora=self.data.diaspora)
self.data.results = JewishCalendarDataResults(dateinfo, zmanim)

View File

@@ -19,7 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
import homeassistant.util.dt as dt_util
from homeassistant.util import dt as dt_util
from .entity import JewishCalendarConfigEntry, JewishCalendarEntity
@@ -236,18 +236,25 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
return []
return [self.entity_description.next_update_fn(zmanim)]
def get_dateinfo(self) -> HDateInfo:
def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
"""Get the next date info."""
now = dt_util.now()
if self.data.results is None:
self.create_results()
assert self.data.results is not None, "Results should be available"
if now is None:
now = dt_util.now()
today = now.date()
zmanim = self.make_zmanim(today)
update = None
if self.entity_description.next_update_fn:
update = self.entity_description.next_update_fn(self.coordinator.zmanim)
update = self.entity_description.next_update_fn(zmanim)
_LOGGER.debug("Today: %s, update: %s", now.date(), update)
_LOGGER.debug("Today: %s, update: %s", today, update)
if update is not None and now >= update:
return self.coordinator.dateinfo.next_day
return self.coordinator.dateinfo
return self.data.results.dateinfo.next_day
return self.data.results.dateinfo
class JewishCalendarSensor(JewishCalendarBaseSensor):
@@ -264,9 +271,7 @@ class JewishCalendarSensor(JewishCalendarBaseSensor):
super().__init__(config_entry, description)
# Set the options for enumeration sensors
if self.entity_description.options_fn is not None:
self._attr_options = self.entity_description.options_fn(
self.coordinator.data.diaspora
)
self._attr_options = self.entity_description.options_fn(self.data.diaspora)
@property
def native_value(self) -> str | int | dt.datetime | None:
@@ -290,8 +295,9 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@property
def native_value(self) -> dt.datetime | None:
"""Return the state of the sensor."""
if self.data.results is None:
self.create_results()
assert self.data.results is not None, "Results should be available"
if self.entity_description.value_fn is None:
return self.coordinator.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(
self.get_dateinfo(), self.coordinator.make_zmanim
)
return self.data.results.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim)

View File

@@ -285,13 +285,19 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
group_address_switch_green_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_GREEN_SWITCH
),
group_address_brightness_green=conf.get_write(CONF_GA_GREEN_BRIGHTNESS),
group_address_brightness_green=conf.get_write(
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
),
group_address_brightness_green_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_GREEN_BRIGHTNESS
),
group_address_switch_blue=conf.get_write(CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=conf.get_state_and_passive(CONF_GA_BLUE_SWITCH),
group_address_brightness_blue=conf.get_write(CONF_GA_BLUE_BRIGHTNESS),
group_address_switch_blue=conf.get_write(CONF_COLOR, CONF_GA_BLUE_SWITCH),
group_address_switch_blue_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_BLUE_SWITCH
),
group_address_brightness_blue=conf.get_write(
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
),
group_address_brightness_blue_state=conf.get_state_and_passive(
CONF_COLOR, CONF_GA_BLUE_BRIGHTNESS
),

View File

@@ -13,11 +13,12 @@ from homeassistant.util.ulid import ulid_now
from ..const import DOMAIN
from .const import CONF_DATA
from .migration import migrate_1_to_2
from .migration import migrate_1_to_2, migrate_2_1_to_2_2
_LOGGER = logging.getLogger(__name__)
STORAGE_VERSION: Final = 2
STORAGE_VERSION_MINOR: Final = 2
STORAGE_KEY: Final = f"{DOMAIN}/config_store.json"
type KNXPlatformStoreModel = dict[str, dict[str, Any]] # unique_id: configuration
@@ -54,9 +55,13 @@ class _KNXConfigStoreStorage(Store[KNXConfigStoreModel]):
) -> dict[str, Any]:
"""Migrate to the new version."""
if old_major_version == 1:
# version 2 introduced in 2025.8
# version 2.1 introduced in 2025.8
migrate_1_to_2(old_data)
if old_major_version <= 2 and old_minor_version < 2:
# version 2.2 introduced in 2025.9.2
migrate_2_1_to_2_2(old_data)
return old_data
@@ -71,7 +76,9 @@ class KNXConfigStore:
"""Initialize config store."""
self.hass = hass
self.config_entry = config_entry
self._store = _KNXConfigStoreStorage(hass, STORAGE_VERSION, STORAGE_KEY)
self._store = _KNXConfigStoreStorage(
hass, STORAGE_VERSION, STORAGE_KEY, minor_version=STORAGE_VERSION_MINOR
)
self.data = KNXConfigStoreModel(entities={})
self._platform_controllers: dict[Platform, PlatformControllerBase] = {}

View File

@@ -240,19 +240,19 @@ LIGHT_KNX_SCHEMA = AllSerializeFirst(
write_required=True, valid_dpt="5.001"
),
"section_blue": KNXSectionFlat(),
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
vol.Optional(CONF_GA_BLUE_SWITCH): GASelector(
write_required=False, valid_dpt="1"
),
"section_white": KNXSectionFlat(),
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
vol.Required(CONF_GA_BLUE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
"section_white": KNXSectionFlat(),
vol.Optional(CONF_GA_WHITE_SWITCH): GASelector(
write_required=False, valid_dpt="1"
),
vol.Optional(CONF_GA_WHITE_BRIGHTNESS): GASelector(
write_required=True, valid_dpt="5.001"
),
},
),
GroupSelectOption(

View File

@@ -4,6 +4,7 @@ from typing import Any
from homeassistant.const import Platform
from ..const import CONF_RESPOND_TO_READ
from . import const as store_const
@@ -40,3 +41,12 @@ def _migrate_light_schema_1_to_2(light_knx_data: dict[str, Any]) -> None:
if color:
light_knx_data[store_const.CONF_COLOR] = color
def migrate_2_1_to_2_2(data: dict[str, Any]) -> None:
"""Migrate from schema 2.1 to schema 2.2."""
if b_sensors := data.get("entities", {}).get(Platform.BINARY_SENSOR):
for b_sensor in b_sensors.values():
# "respond_to_read" was never used for binary_sensor and is not valid
# in the new schema. It was set as default in Store schema v1 and v2.1
b_sensor["knx"].pop(CONF_RESPOND_TO_READ, None)

View File

@@ -54,6 +54,6 @@
"requirements": [
"aiolifx==1.2.1",
"aiolifx-effects==0.3.2",
"aiolifx-themes==0.6.4"
"aiolifx-themes==1.0.2"
]
}

View File

@@ -57,7 +57,8 @@
},
"extra_fields": {
"brightness_pct": "Brightness",
"flash": "Flash"
"flash": "Flash",
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -634,8 +634,8 @@ DISCOVERY_SCHEMAS = [
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="NitrogenDioxideSensor",
translation_key="nitrogen_dioxide",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
device_class=SensorDeviceClass.NITROGEN_DIOXIDE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,

View File

@@ -435,6 +435,9 @@
"evse_soc": {
"name": "State of charge"
},
"nitrogen_dioxide": {
"name": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]"
},
"pump_control_mode": {
"name": "Control mode",
"state": {

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp[default]==2025.08.11"],
"requirements": ["yt-dlp[default]==2025.09.05"],
"single_config_entry": true
}

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.12.5", "mill-local==0.3.0"]
"requirements": ["millheater==0.13.1", "mill-local==0.3.0"]
}

View File

@@ -267,8 +267,8 @@ CLIMATE_SCHEMA = vol.All(
{
vol.Required(CONF_TARGET_TEMP): hvac_fixedsize_reglist_validator,
vol.Optional(CONF_TARGET_TEMP_WRITE_REGISTERS, default=False): cv.boolean,
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(float),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(float),
vol.Optional(CONF_MAX_TEMP, default=35): vol.Coerce(int),
vol.Optional(CONF_MIN_TEMP, default=5): vol.Coerce(int),
vol.Optional(CONF_STEP, default=0.5): vol.Coerce(float),
vol.Optional(CONF_TEMPERATURE_UNIT, default=DEFAULT_TEMP_UNIT): cv.string,
vol.Exclusive(CONF_HVAC_ONOFF_COIL, "hvac_onoff_type"): cv.positive_int,

View File

@@ -62,7 +62,6 @@ from .const import (
CONF_VIRTUAL_COUNT,
CONF_WRITE_TYPE,
CONF_ZERO_SUPPRESS,
SIGNAL_START_ENTITY,
SIGNAL_STOP_ENTITY,
DataType,
)
@@ -95,18 +94,10 @@ class BasePlatform(Entity):
self._attr_name = entry[CONF_NAME]
self._attr_device_class = entry.get(CONF_DEVICE_CLASS)
def get_optional_numeric_config(config_name: str) -> int | float | None:
if (val := entry.get(config_name)) is None:
return None
assert isinstance(val, (float, int)), (
f"Expected float or int but {config_name} was {type(val)}"
)
return val
self._min_value = get_optional_numeric_config(CONF_MIN_VALUE)
self._max_value = get_optional_numeric_config(CONF_MAX_VALUE)
self._min_value = entry.get(CONF_MIN_VALUE)
self._max_value = entry.get(CONF_MAX_VALUE)
self._nan_value = entry.get(CONF_NAN_VALUE)
self._zero_suppress = get_optional_numeric_config(CONF_ZERO_SUPPRESS)
self._zero_suppress = entry.get(CONF_ZERO_SUPPRESS)
@abstractmethod
async def _async_update(self) -> None:
@@ -143,7 +134,6 @@ class BasePlatform(Entity):
self._cancel_call()
self._cancel_call = None
self._attr_available = False
self.async_write_ha_state()
async def async_await_connection(self, _now: Any) -> None:
"""Wait for first connect."""
@@ -162,11 +152,6 @@ class BasePlatform(Entity):
self.async_on_remove(
async_dispatcher_connect(self.hass, SIGNAL_STOP_ENTITY, self.async_disable)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_START_ENTITY, self.async_local_update
)
)
class BaseStructPlatform(BasePlatform, RestoreEntity):
@@ -352,7 +337,6 @@ class BaseSwitch(BasePlatform, ToggleEntity, RestoreEntity):
return
if self._verify_delay:
assert self._verify_delay == 1
if self._cancel_call:
self._cancel_call()
self._cancel_call = None

View File

@@ -64,7 +64,8 @@ class ModbusLight(BaseSwitch, LightEntity):
self._attr_color_mode = self._detect_color_mode(config)
self._attr_supported_color_modes = {self._attr_color_mode}
# Set min/max kelvin values if the mode is COLOR_TEMP
self._attr_min_color_temp_kelvin: int = LIGHT_DEFAULT_MIN_KELVIN
self._attr_max_color_temp_kelvin: int = LIGHT_DEFAULT_MAX_KELVIN
if self._attr_color_mode == ColorMode.COLOR_TEMP:
self._attr_min_color_temp_kelvin = config.get(
CONF_MIN_TEMP, LIGHT_DEFAULT_MIN_KELVIN
@@ -193,9 +194,6 @@ class ModbusLight(BaseSwitch, LightEntity):
def _convert_modbus_percent_to_temperature(self, percent: int) -> int:
"""Convert Modbus scale (0-100) to the color temperature in Kelvin (2000-7000 К)."""
assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance(
self._attr_max_color_temp_kelvin, int
)
return round(
self._attr_min_color_temp_kelvin
+ (
@@ -216,9 +214,6 @@ class ModbusLight(BaseSwitch, LightEntity):
def _convert_color_temp_to_modbus(self, kelvin: int) -> int:
"""Convert color temperature from Kelvin to the Modbus scale (0-100)."""
assert isinstance(self._attr_min_color_temp_kelvin, int) and isinstance(
self._attr_max_color_temp_kelvin, int
)
return round(
LIGHT_MODBUS_SCALE_MIN
+ (kelvin - self._attr_min_color_temp_kelvin)

View File

@@ -223,6 +223,8 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
# Brightness is supported and no supported_color_modes are set,
# so set brightness as the supported color mode.
self._attr_supported_color_modes = {ColorMode.BRIGHTNESS}
else:
self._attr_supported_color_modes = {ColorMode.ONOFF}
def _update_color(self, values: dict[str, Any]) -> None:
color_mode: str = values["color_mode"]

View File

@@ -7,5 +7,5 @@
"iot_class": "cloud_push",
"loggers": ["aionfty"],
"quality_scale": "bronze",
"requirements": ["aiontfy==0.5.4"]
"requirements": ["aiontfy==0.5.5"]
}

View File

@@ -10,7 +10,7 @@ import logging
from ohme import ApiException, OhmeApiClient
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -83,6 +83,21 @@ class OhmeAdvancedSettingsCoordinator(OhmeBaseCoordinator):
coordinator_name = "Advanced Settings"
def __init__(
self, hass: HomeAssistant, config_entry: OhmeConfigEntry, client: OhmeApiClient
) -> None:
"""Initialise coordinator."""
super().__init__(hass, config_entry, client)
@callback
def _dummy_listener() -> None:
pass
# This coordinator is used by the API library to determine whether the
# charger is online and available. It is therefore required even if no
# entities are using it.
self.async_add_listener(_dummy_listener)
async def _internal_update_data(self) -> None:
"""Fetch data from API endpoint."""
await self.client.async_get_advanced_settings()

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.5.1"]
"requirements": ["ohme==1.5.2"]
}

View File

@@ -181,7 +181,7 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[OWMUpdateCoordinator]
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING)
@property
def visibility(self) -> float | str | None:
def native_visibility(self) -> float | None:
"""Return visibility."""
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE)

View File

@@ -14,6 +14,9 @@
"changed_states": "[%key:common::device_automation::trigger_type::changed_states%]",
"turned_on": "[%key:common::device_automation::trigger_type::turned_on%]",
"turned_off": "[%key:common::device_automation::trigger_type::turned_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/schlage",
"iot_class": "cloud_polling",
"requirements": ["pyschlage==2025.7.3"]
"requirements": ["pyschlage==2025.9.0"]
}

View File

@@ -365,7 +365,7 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
unit converter supports both the native and the suggested units of measurement.
"""
# Make sure we can convert the units
if (
if self.native_unit_of_measurement != suggested_unit_of_measurement and (
(unit_converter := UNIT_CONVERTERS.get(self.device_class)) is None
or self.__native_unit_of_measurement_compat
not in unit_converter.VALID_UNITS

View File

@@ -3,6 +3,7 @@
import asyncio
from contextlib import suppress
import aiohttp
from sharkiq import (
AylaApi,
SharkIqAuthError,
@@ -15,7 +16,7 @@ from homeassistant import exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
API_TIMEOUT,
@@ -56,10 +57,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
data={**config_entry.data, CONF_REGION: SHARKIQ_REGION_DEFAULT},
)
new_websession = async_create_clientsession(
hass,
cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False),
)
ayla_api = get_ayla_api(
username=config_entry.data[CONF_USERNAME],
password=config_entry.data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
websession=new_websession,
europe=(config_entry.data[CONF_REGION] == SHARKIQ_REGION_EUROPE),
)
@@ -94,7 +100,7 @@ async def async_disconnect_or_timeout(coordinator: SharkIqUpdateCoordinator):
await coordinator.ayla_api.async_sign_out()
async def async_update_options(hass, config_entry):
async def async_update_options(hass: HomeAssistant, config_entry):
"""Update options."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import (
DOMAIN,
@@ -44,15 +44,19 @@ async def _validate_input(
hass: HomeAssistant, data: Mapping[str, Any]
) -> dict[str, str]:
"""Validate the user input allows us to connect."""
new_websession = async_create_clientsession(
hass,
cookie_jar=aiohttp.CookieJar(unsafe=True, quote_cookie=False),
)
ayla_api = get_ayla_api(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
websession=async_get_clientsession(hass),
websession=new_websession,
europe=(data[CONF_REGION] == SHARKIQ_REGION_EUROPE),
)
try:
async with asyncio.timeout(10):
async with asyncio.timeout(15):
LOGGER.debug("Initialize connection to Ayla networks API")
await ayla_api.async_sign_in()
except (TimeoutError, aiohttp.ClientError, TypeError) as error:

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/sharkiq",
"iot_class": "cloud_polling",
"loggers": ["sharkiq"],
"requirements": ["sharkiq==1.1.1"]
"requirements": ["sharkiq==1.4.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pymodbus", "pysmarty2"],
"requirements": ["pysmarty2==0.10.2"]
"requirements": ["pysmarty2==0.10.3"]
}

View File

@@ -61,8 +61,15 @@ async def async_setup_entry(
if (
state := getattr(speaker.soco, select_data.soco_attribute, None)
) is not None:
setattr(speaker, select_data.speaker_attribute, state)
features.append(select_data)
try:
setattr(speaker, select_data.speaker_attribute, int(state))
features.append(select_data)
except ValueError:
_LOGGER.error(
"Invalid value for %s %s",
select_data.speaker_attribute,
state,
)
return features
async def _async_create_entities(speaker: SonosSpeaker) -> None:

View File

@@ -599,7 +599,12 @@ class SonosSpeaker:
for enum_var in (ATTR_DIALOG_LEVEL,):
if enum_var in variables:
setattr(self, f"{enum_var}_enum", variables[enum_var])
try:
setattr(self, f"{enum_var}_enum", int(variables[enum_var]))
except ValueError:
_LOGGER.error(
"Invalid value for %s %s", enum_var, variables[enum_var]
)
self.async_write_entity_states()

View File

@@ -116,6 +116,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
MediaType.APPS: MediaType.APP,
MediaType.APP: MediaType.TRACK,
"favorite": None,
"track": MediaType.TRACK,
}

View File

@@ -607,7 +607,7 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
_media_content_type_list = (
query.media_content_type.lower().replace(", ", ",").split(",")
if query.media_content_type
else ["albums", "tracks", "artists", "genres"]
else ["albums", "tracks", "artists", "genres", "playlists"]
)
if query.media_content_type and set(_media_content_type_list).difference(

View File

@@ -14,6 +14,9 @@
"changed_states": "[%key:common::device_automation::trigger_type::changed_states%]",
"turned_on": "[%key:common::device_automation::trigger_type::turned_on%]",
"turned_off": "[%key:common::device_automation::trigger_type::turned_off%]"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -291,6 +291,7 @@ class TractiveClient:
for switch, key in SWITCH_KEY_MAP.items():
if switch_data := event.get(key):
payload[switch] = switch_data["active"]
payload[ATTR_POWER_SAVING] = event.get("tracker_state_reason") == "POWER_SAVING"
self._dispatch_tracker_event(
TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload
)

View File

@@ -18,6 +18,7 @@ from .const import (
ATTR_BUZZER,
ATTR_LED,
ATTR_LIVE_TRACKING,
ATTR_POWER_SAVING,
TRACKER_SWITCH_STATUS_UPDATED,
)
from .entity import TractiveEntity
@@ -104,7 +105,7 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
# We received an event, so the service is online and the switch entities should
# be available.
self._attr_available = True
self._attr_available = not event[ATTR_POWER_SAVING]
self._attr_is_on = event[self.entity_description.key]
self.async_write_ha_state()

View File

@@ -42,6 +42,6 @@
"documentation": "https://www.home-assistant.io/integrations/tuya",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["tuya_iot"],
"loggers": ["tuya_sharing"],
"requirements": ["tuya-device-sharing-sdk==0.2.1"]
}

View File

@@ -5,6 +5,9 @@
"changed_states": "{entity_name} update availability changed",
"turned_on": "{entity_name} got an update available",
"turned_off": "{entity_name} became up-to-date"
},
"extra_fields": {
"for": "[%key:common::device_automation::extra_fields::for%]"
}
},
"entity_component": {

View File

@@ -59,5 +59,6 @@ class VeluxRainSensor(VeluxEntity, BinarySensorEntity):
LOGGER.error("Error fetching limitation data for cover %s", self.name)
return
# Velux windows with rain sensors report an opening limitation of 93 when rain is detected.
self._attr_is_on = limitation.min_value == 93
# Velux windows with rain sensors report an opening limitation of 93 or 100 (Velux GPU) when rain is detected.
# So far, only 93 and 100 have been observed in practice, documentation on this is non-existent AFAIK.
self._attr_is_on = limitation.min_value in {93, 100}

View File

@@ -4,8 +4,15 @@ from __future__ import annotations
from typing import Any, cast
from pyvlx import OpeningDevice, Position
from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter
from pyvlx import (
Awning,
Blind,
GarageDoor,
Gate,
OpeningDevice,
Position,
RollerShutter,
)
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -97,7 +104,10 @@ class VeluxCover(VeluxEntity, CoverEntity):
@property
def is_closed(self) -> bool:
"""Return if the cover is closed."""
return self.node.position.closed
# do not use the node's closed state but rely on cover position
# until https://github.com/Julius2342/pyvlx/pull/543 is merged.
# once merged this can again return self.node.position.closed
return self.current_cover_position == 0
@property
def is_opening(self) -> bool:

View File

@@ -1,7 +1,7 @@
{
"domain": "velux",
"name": "Velux",
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio"],
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio", "@wollew"],
"config_flow": true,
"dhcp": [
{

View File

@@ -8,11 +8,14 @@ from typing import Any, cast
from aiohttp import ClientSession
from aiovodafone import VodafoneStationDevice, VodafoneStationSercommApi, exceptions
from homeassistant.components.device_tracker import DEFAULT_CONSIDER_HOME
from homeassistant.components.device_tracker import (
DEFAULT_CONSIDER_HOME,
DOMAIN as DEVICE_TRACKER_DOMAIN,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -71,16 +74,14 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
update_interval=timedelta(seconds=SCAN_INTERVAL),
config_entry=config_entry,
)
device_reg = dr.async_get(self.hass)
device_list = dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
)
entity_reg = er.async_get(hass)
self.previous_devices = {
connection[1].upper()
for device in device_list
for connection in device.connections
if connection[0] == dr.CONNECTION_NETWORK_MAC
entry.unique_id
for entry in er.async_entries_for_config_entry(
entity_reg, config_entry.entry_id
)
if entry.domain == DEVICE_TRACKER_DOMAIN
}
def _calculate_update_time_and_consider_home(

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"quality_scale": "platinum",
"requirements": ["aiovodafone==0.10.0"]
"requirements": ["aiovodafone==1.2.1"]
}

View File

@@ -146,6 +146,8 @@ async def async_send_message( # noqa: C901
self.enable_starttls = use_tls
self.enable_direct_tls = use_tls
self.enable_plaintext = not use_tls
self["feature_mechanisms"].unencrypted_scram = not use_tls
self.use_ipv6 = False
self.add_event_handler("failed_all_auth", self.disconnect_on_login_fail)
self.add_event_handler("session_start", self.start)

View File

@@ -25,7 +25,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2025
MINOR_VERSION: Final = 9
PATCH_VERSION: Final = "0"
PATCH_VERSION: Final = "3"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 13, 2)

View File

@@ -1898,11 +1898,25 @@ def _async_setup_entity_restore(hass: HomeAssistant, registry: EntityRegistry) -
@callback
def cleanup_restored_states_filter(event_data: Mapping[str, Any]) -> bool:
"""Clean up restored states filter."""
return bool(event_data["action"] == "remove")
return (event_data["action"] == "remove") or (
event_data["action"] == "update"
and "old_entity_id" in event_data
and event_data["entity_id"] != event_data["old_entity_id"]
)
@callback
def cleanup_restored_states(event: Event[EventEntityRegistryUpdatedData]) -> None:
"""Clean up restored states."""
if event.data["action"] == "update":
old_entity_id = event.data["old_entity_id"]
old_state = hass.states.get(old_entity_id)
if old_state is None or not old_state.attributes.get(ATTR_RESTORED):
return
hass.states.async_remove(old_entity_id, context=event.context)
if entry := registry.async_get(event.data["entity_id"]):
entry.write_unavailable_state(hass)
return
state = hass.states.get(event.data["entity_id"])
if state is None or not state.attributes.get(ATTR_RESTORED):

View File

@@ -34,11 +34,11 @@ dbus-fast==2.44.3
fnv-hash-fast==1.5.0
go2rtc-client==0.2.1
ha-ffmpeg==3.2.2
habluetooth==5.3.0
hass-nabucasa==1.1.0
habluetooth==5.6.4
hass-nabucasa==1.1.1
hassil==3.2.0
home-assistant-bluetooth==1.13.1
home-assistant-frontend==20250903.2
home-assistant-frontend==20250903.5
home-assistant-intents==2025.9.3
httpx==0.28.1
ifaddr==0.2.0

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "homeassistant"
version = "2025.9.0"
version = "2025.9.3"
license = "Apache-2.0"
license-files = ["LICENSE*", "homeassistant/backports/LICENSE*"]
description = "Open-source home automation platform running on Python 3."
@@ -47,7 +47,7 @@ dependencies = [
"fnv-hash-fast==1.5.0",
# hass-nabucasa is imported by helpers which don't depend on the cloud
# integration
"hass-nabucasa==1.1.0",
"hass-nabucasa==1.1.1",
# When bumping httpx, please check the version pins of
# httpcore, anyio, and h11 in gen_requirements_all
"httpx==0.28.1",

2
requirements.txt generated
View File

@@ -22,7 +22,7 @@ certifi>=2021.5.30
ciso8601==2.3.2
cronsim==2.6
fnv-hash-fast==1.5.0
hass-nabucasa==1.1.0
hass-nabucasa==1.1.1
httpx==0.28.1
home-assistant-bluetooth==1.13.1
ifaddr==0.2.0

42
requirements_all.txt generated
View File

@@ -131,7 +131,7 @@ TwitterAPI==2.7.12
WSDiscovery==2.1.2
# homeassistant.components.accuweather
accuweather==4.2.0
accuweather==4.2.1
# homeassistant.components.adax
adax==0.4.0
@@ -238,7 +238,7 @@ aioeafm==0.1.2
aioeagle==1.1.0
# homeassistant.components.ecowitt
aioecowitt==2025.9.0
aioecowitt==2025.9.1
# homeassistant.components.co2signal
aioelectricitymaps==1.1.1
@@ -262,7 +262,7 @@ aiogithubapi==24.6.0
aioguardian==2022.07.0
# homeassistant.components.harmony
aioharmony==0.5.2
aioharmony==0.5.3
# homeassistant.components.hassio
aiohasupervisor==0.3.2
@@ -277,7 +277,7 @@ aiohomekit==3.2.15
aiohttp_sse==2.2.0
# homeassistant.components.hue
aiohue==4.7.4
aiohue==4.7.5
# homeassistant.components.imap
aioimaplib==2.0.1
@@ -298,7 +298,7 @@ aiokem==1.0.1
aiolifx-effects==0.3.2
# homeassistant.components.lifx
aiolifx-themes==0.6.4
aiolifx-themes==1.0.2
# homeassistant.components.lifx
aiolifx==1.2.1
@@ -325,7 +325,7 @@ aionanoleaf==0.2.1
aionotion==2024.03.0
# homeassistant.components.ntfy
aiontfy==0.5.4
aiontfy==0.5.5
# homeassistant.components.nut
aionut==4.3.4
@@ -426,7 +426,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1
# homeassistant.components.vodafone_station
aiovodafone==0.10.0
aiovodafone==1.2.1
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -618,14 +618,14 @@ beautifulsoup4==4.13.3
# beewi-smartclim==0.0.10
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.17.2
bimmer-connected[china]==0.17.3
# homeassistant.components.bizkaibus
bizkaibus==0.1.1
# homeassistant.components.eq3btsmart
# homeassistant.components.esphome
bleak-esphome==3.2.0
bleak-esphome==3.3.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.4.3
@@ -1002,7 +1002,7 @@ gassist-text==0.0.14
gcal-sync==8.0.0
# homeassistant.components.aladdin_connect
genie-partner-sdk==1.0.10
genie-partner-sdk==1.0.11
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -1134,10 +1134,10 @@ ha-silabs-firmware-client==0.2.0
habiticalib==0.4.3
# homeassistant.components.bluetooth
habluetooth==5.3.0
habluetooth==5.6.4
# homeassistant.components.cloud
hass-nabucasa==1.1.0
hass-nabucasa==1.1.1
# homeassistant.components.splunk
hass-splunk==0.1.1
@@ -1178,7 +1178,7 @@ hole==0.9.0
holidays==0.79
# homeassistant.components.frontend
home-assistant-frontend==20250903.2
home-assistant-frontend==20250903.5
# homeassistant.components.conversation
home-assistant-intents==2025.9.3
@@ -1241,7 +1241,7 @@ igloohome-api==0.1.1
ihcsdk==2.8.5
# homeassistant.components.imeon_inverter
imeon_inverter_api==0.3.14
imeon_inverter_api==0.3.16
# homeassistant.components.imgw_pib
imgw_pib==1.5.4
@@ -1441,7 +1441,7 @@ microBeesPy==0.3.5
mill-local==0.3.0
# homeassistant.components.mill
millheater==0.12.5
millheater==0.13.1
# homeassistant.components.minio
minio==7.1.12
@@ -1580,7 +1580,7 @@ odp-amsterdam==6.1.2
oemthermostat==1.1.1
# homeassistant.components.ohme
ohme==1.5.1
ohme==1.5.2
# homeassistant.components.ollama
ollama==0.5.1
@@ -1934,7 +1934,7 @@ pydiscovergy==3.0.2
pydoods==1.0.2
# homeassistant.components.hydrawise
pydrawise==2025.7.0
pydrawise==2025.9.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==3.0.0
@@ -2312,7 +2312,7 @@ pysabnzbd==1.1.1
pysaj==0.0.16
# homeassistant.components.schlage
pyschlage==2025.7.3
pyschlage==2025.9.0
# homeassistant.components.sensibo
pysensibo==1.2.1
@@ -2354,7 +2354,7 @@ pysmarlaapi==0.9.2
pysmartthings==3.2.9
# homeassistant.components.smarty
pysmarty2==0.10.2
pysmarty2==0.10.3
# homeassistant.components.smhi
pysmhi==1.0.2
@@ -2768,7 +2768,7 @@ sentry-sdk==1.45.1
sfrbox-api==0.0.12
# homeassistant.components.sharkiq
sharkiq==1.1.1
sharkiq==1.4.0
# homeassistant.components.aquostv
sharp_aquos_rc==0.3.2
@@ -3184,7 +3184,7 @@ youless-api==2.2.0
youtubeaio==2.0.0
# homeassistant.components.media_extractor
yt-dlp[default]==2025.08.11
yt-dlp[default]==2025.09.05
# homeassistant.components.zabbix
zabbix-utils==2.0.2

View File

@@ -119,7 +119,7 @@ Tami4EdgeAPI==3.0
WSDiscovery==2.1.2
# homeassistant.components.accuweather
accuweather==4.2.0
accuweather==4.2.1
# homeassistant.components.adax
adax==0.4.0
@@ -226,7 +226,7 @@ aioeafm==0.1.2
aioeagle==1.1.0
# homeassistant.components.ecowitt
aioecowitt==2025.9.0
aioecowitt==2025.9.1
# homeassistant.components.co2signal
aioelectricitymaps==1.1.1
@@ -247,7 +247,7 @@ aiogithubapi==24.6.0
aioguardian==2022.07.0
# homeassistant.components.harmony
aioharmony==0.5.2
aioharmony==0.5.3
# homeassistant.components.hassio
aiohasupervisor==0.3.2
@@ -262,7 +262,7 @@ aiohomekit==3.2.15
aiohttp_sse==2.2.0
# homeassistant.components.hue
aiohue==4.7.4
aiohue==4.7.5
# homeassistant.components.imap
aioimaplib==2.0.1
@@ -280,7 +280,7 @@ aiokem==1.0.1
aiolifx-effects==0.3.2
# homeassistant.components.lifx
aiolifx-themes==0.6.4
aiolifx-themes==1.0.2
# homeassistant.components.lifx
aiolifx==1.2.1
@@ -307,7 +307,7 @@ aionanoleaf==0.2.1
aionotion==2024.03.0
# homeassistant.components.ntfy
aiontfy==0.5.4
aiontfy==0.5.5
# homeassistant.components.nut
aionut==4.3.4
@@ -408,7 +408,7 @@ aiousbwatcher==1.1.1
aiovlc==0.5.1
# homeassistant.components.vodafone_station
aiovodafone==0.10.0
aiovodafone==1.2.1
# homeassistant.components.waqi
aiowaqi==3.1.0
@@ -555,11 +555,11 @@ base36==0.1.1
beautifulsoup4==4.13.3
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.17.2
bimmer-connected[china]==0.17.3
# homeassistant.components.eq3btsmart
# homeassistant.components.esphome
bleak-esphome==3.2.0
bleak-esphome==3.3.0
# homeassistant.components.bluetooth
bleak-retry-connector==4.4.3
@@ -872,7 +872,7 @@ gassist-text==0.0.14
gcal-sync==8.0.0
# homeassistant.components.aladdin_connect
genie-partner-sdk==1.0.10
genie-partner-sdk==1.0.11
# homeassistant.components.geniushub
geniushub-client==0.7.1
@@ -995,10 +995,10 @@ ha-silabs-firmware-client==0.2.0
habiticalib==0.4.3
# homeassistant.components.bluetooth
habluetooth==5.3.0
habluetooth==5.6.4
# homeassistant.components.cloud
hass-nabucasa==1.1.0
hass-nabucasa==1.1.1
# homeassistant.components.assist_satellite
# homeassistant.components.conversation
@@ -1027,7 +1027,7 @@ hole==0.9.0
holidays==0.79
# homeassistant.components.frontend
home-assistant-frontend==20250903.2
home-assistant-frontend==20250903.5
# homeassistant.components.conversation
home-assistant-intents==2025.9.3
@@ -1075,7 +1075,7 @@ ifaddr==0.2.0
igloohome-api==0.1.1
# homeassistant.components.imeon_inverter
imeon_inverter_api==0.3.14
imeon_inverter_api==0.3.16
# homeassistant.components.imgw_pib
imgw_pib==1.5.4
@@ -1233,7 +1233,7 @@ microBeesPy==0.3.5
mill-local==0.3.0
# homeassistant.components.mill
millheater==0.12.5
millheater==0.13.1
# homeassistant.components.minio
minio==7.1.12
@@ -1348,7 +1348,7 @@ objgraph==3.5.0
odp-amsterdam==6.1.2
# homeassistant.components.ohme
ohme==1.5.1
ohme==1.5.2
# homeassistant.components.ollama
ollama==0.5.1
@@ -1615,7 +1615,7 @@ pydexcom==0.2.3
pydiscovergy==3.0.2
# homeassistant.components.hydrawise
pydrawise==2025.7.0
pydrawise==2025.9.0
# homeassistant.components.android_ip_webcam
pydroid-ipcam==3.0.0
@@ -1924,7 +1924,7 @@ pyrympro==0.0.9
pysabnzbd==1.1.1
# homeassistant.components.schlage
pyschlage==2025.7.3
pyschlage==2025.9.0
# homeassistant.components.sensibo
pysensibo==1.2.1
@@ -1957,7 +1957,7 @@ pysmarlaapi==0.9.2
pysmartthings==3.2.9
# homeassistant.components.smarty
pysmarty2==0.10.2
pysmarty2==0.10.3
# homeassistant.components.smhi
pysmhi==1.0.2
@@ -2290,7 +2290,7 @@ sentry-sdk==1.45.1
sfrbox-api==0.0.12
# homeassistant.components.sharkiq
sharkiq==1.1.1
sharkiq==1.4.0
# homeassistant.components.simplefin
simplefin4py==0.0.18
@@ -2631,7 +2631,7 @@ youless-api==2.2.0
youtubeaio==2.0.0
# homeassistant.components.media_extractor
yt-dlp[default]==2025.08.11
yt-dlp[default]==2025.09.05
# homeassistant.components.zamg
zamg==0.3.6

View File

@@ -30,24 +30,6 @@ async def test_show_form(hass: HomeAssistant) -> None:
assert result["step_id"] == "user"
async def test_api_key_too_short(hass: HomeAssistant) -> None:
"""Test that errors are shown when API key is too short."""
# The API key length check is done by the library without polling the AccuWeather
# server so we don't need to patch the library method.
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_USER},
data={
CONF_NAME: "abcd",
CONF_API_KEY: "foo",
CONF_LATITUDE: 55.55,
CONF_LONGITUDE: 122.12,
},
)
assert result["errors"] == {CONF_API_KEY: "invalid_api_key"}
async def test_invalid_api_key(
hass: HomeAssistant, mock_accuweather_client: AsyncMock
) -> None:

View File

@@ -30,6 +30,7 @@ async def test_setup_entry(hass: HomeAssistant) -> None:
mock_door.status = "closed"
mock_door.link_status = "connected"
mock_door.battery_level = 100
mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}"
mock_client = AsyncMock()
mock_client.get_doors.return_value = [mock_door]
@@ -80,6 +81,7 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
mock_door.status = "closed"
mock_door.link_status = "connected"
mock_door.battery_level = 100
mock_door.unique_id = f"{mock_door.device_id}-{mock_door.door_number}"
# Mock client
mock_client = AsyncMock()

View File

@@ -7,7 +7,11 @@ from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
import pytest
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.components.alexa_devices.const import (
CONF_LOGIN_DATA,
CONF_SITE,
DOMAIN,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
@@ -41,6 +45,7 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]:
client = mock_client.return_value
client.login_mode_interactive.return_value = {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
}
client.get_devices_data.return_value = {
TEST_SERIAL_NUMBER: AmazonDevice(
@@ -81,9 +86,12 @@ def mock_config_entry() -> MockConfigEntry:
data={
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {"session": "test-session"},
CONF_LOGIN_DATA: {
"session": "test-session",
CONF_SITE: "https://www.amazon.com",
},
},
unique_id=TEST_USERNAME,
version=1,
minor_version=2,
minor_version=3,
)

View File

@@ -49,6 +49,7 @@
'data': dict({
'login_data': dict({
'session': 'test-session',
'site': 'https://www.amazon.com',
}),
'password': '**REDACTED**',
'username': '**REDACTED**',
@@ -57,7 +58,7 @@
'discovery_keys': dict({
}),
'domain': 'alexa_devices',
'minor_version': 2,
'minor_version': 3,
'options': dict({
}),
'pref_disable_new_entities': False,

View File

@@ -9,7 +9,11 @@ from aioamazondevices.exceptions import (
)
import pytest
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.components.alexa_devices.const import (
CONF_LOGIN_DATA,
CONF_SITE,
DOMAIN,
)
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
@@ -48,6 +52,7 @@ async def test_full_flow(
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
},
}
assert result["result"].unique_id == TEST_USERNAME
@@ -158,6 +163,16 @@ async def test_reauth_successful(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data == {
CONF_CODE: "000000",
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: "other_fake_password",
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
},
}
@pytest.mark.parametrize(
("side_effect", "error"),
@@ -206,8 +221,15 @@ async def test_reauth_not_successful(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_PASSWORD] == "fake_password"
assert mock_config_entry.data[CONF_CODE] == "111111"
assert mock_config_entry.data == {
CONF_CODE: "111111",
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: "fake_password",
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
},
}
async def test_reconfigure_successful(
@@ -240,7 +262,14 @@ async def test_reconfigure_successful(
assert reconfigure_result["reason"] == "reconfigure_successful"
# changed entry
assert mock_config_entry.data[CONF_PASSWORD] == new_password
assert mock_config_entry.data == {
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: new_password,
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
},
}
@pytest.mark.parametrize(
@@ -297,5 +326,6 @@ async def test_reconfigure_fails(
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {
"customer_info": {"user_id": TEST_USERNAME},
CONF_SITE: "https://www.amazon.com",
},
}

View File

@@ -2,9 +2,14 @@
from unittest.mock import AsyncMock
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
from homeassistant.components.alexa_devices.const import (
CONF_LOGIN_DATA,
CONF_SITE,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
@@ -32,24 +37,81 @@ async def test_device_info(
assert device_entry == snapshot
@pytest.mark.parametrize(
("minor_version", "extra_data"),
[
# Standard migration case
(
1,
{
CONF_COUNTRY: "US",
CONF_LOGIN_DATA: {
"session": "test-session",
},
},
),
# Edge case #1: no country, site already in login data, minor version 1
(
1,
{
CONF_LOGIN_DATA: {
"session": "test-session",
CONF_SITE: "https://www.amazon.com",
},
},
),
# Edge case #2: no country, site in data (wrong place), minor version 1
(
1,
{
CONF_SITE: "https://www.amazon.com",
CONF_LOGIN_DATA: {
"session": "test-session",
},
},
),
# Edge case #3: no country, site already in login data, minor version 2
(
2,
{
CONF_LOGIN_DATA: {
"session": "test-session",
CONF_SITE: "https://www.amazon.com",
},
},
),
# Edge case #4: no country, site in data (wrong place), minor version 2
(
2,
{
CONF_SITE: "https://www.amazon.com",
CONF_LOGIN_DATA: {
"session": "test-session",
},
},
),
],
)
async def test_migrate_entry(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
minor_version: int,
extra_data: dict[str, str],
) -> None:
"""Test successful migration of entry data."""
config_entry = MockConfigEntry(
domain=DOMAIN,
title="Amazon Test Account",
data={
CONF_COUNTRY: "US", # country should be in COUNTRY_DOMAINS exceptions
CONF_USERNAME: TEST_USERNAME,
CONF_PASSWORD: TEST_PASSWORD,
CONF_LOGIN_DATA: {"session": "test-session"},
**(extra_data),
},
unique_id=TEST_USERNAME,
version=1,
minor_version=1,
minor_version=minor_version,
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -57,5 +119,5 @@ async def test_migrate_entry(
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert config_entry.state is ConfigEntryState.LOADED
assert config_entry.minor_version == 2
assert config_entry.data[CONF_LOGIN_DATA]["site"] == "https://www.amazon.com"
assert config_entry.minor_version == 3
assert config_entry.data[CONF_LOGIN_DATA][CONF_SITE] == "https://www.amazon.com"

View File

@@ -138,6 +138,7 @@
'state': 'LOW',
}),
]),
'urgent_check_control_messages': None,
}),
'climate': dict({
'activity': 'INACTIVE',
@@ -193,6 +194,24 @@
'state': 'OK',
}),
]),
'next_service_by_distance': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
'next_service_by_time': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
}),
'data': dict({
'attributes': dict({
@@ -1053,6 +1072,7 @@
'state': 'LOW',
}),
]),
'urgent_check_control_messages': None,
}),
'climate': dict({
'activity': 'HEATING',
@@ -1108,6 +1128,24 @@
'state': 'OK',
}),
]),
'next_service_by_distance': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
'next_service_by_time': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
}),
'data': dict({
'attributes': dict({
@@ -1858,6 +1896,7 @@
'state': 'LOW',
}),
]),
'urgent_check_control_messages': None,
}),
'climate': dict({
'activity': 'INACTIVE',
@@ -1922,6 +1961,24 @@
'state': 'OK',
}),
]),
'next_service_by_distance': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
'next_service_by_time': dict({
'due_date': '2024-12-01T00:00:00+00:00',
'due_distance': list([
50000,
'km',
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
}),
'data': dict({
'attributes': dict({
@@ -2621,6 +2678,7 @@
'has_check_control_messages': False,
'messages': list([
]),
'urgent_check_control_messages': None,
}),
'climate': dict({
'activity': 'UNKNOWN',
@@ -2658,6 +2716,16 @@
'state': 'OK',
}),
]),
'next_service_by_distance': None,
'next_service_by_time': dict({
'due_date': '2022-10-01T00:00:00+00:00',
'due_distance': list([
None,
None,
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
}),
'data': dict({
'attributes': dict({
@@ -4991,6 +5059,7 @@
'has_check_control_messages': False,
'messages': list([
]),
'urgent_check_control_messages': None,
}),
'climate': dict({
'activity': 'UNKNOWN',
@@ -5028,6 +5097,16 @@
'state': 'OK',
}),
]),
'next_service_by_distance': None,
'next_service_by_time': dict({
'due_date': '2022-10-01T00:00:00+00:00',
'due_distance': list([
None,
None,
]),
'service_type': 'BRAKE_FLUID',
'state': 'OK',
}),
}),
'data': dict({
'attributes': dict({

View File

@@ -231,6 +231,29 @@ async def test_conversation_agent(hass: HomeAssistant) -> None:
)
@pytest.mark.usefixtures("init_components")
async def test_punctuation(hass: HomeAssistant) -> None:
"""Test punctuation is handled properly."""
hass.states.async_set(
"light.test_light",
"off",
attributes={ATTR_FRIENDLY_NAME: "Test light"},
)
expose_entity(hass, "light.test_light", True)
calls = async_mock_service(hass, "light", "turn_on")
result = await conversation.async_converse(
hass, "Turn?? on,, test;; light!!!", None, Context(), None
)
assert len(calls) == 1
assert calls[0].data["entity_id"][0] == "light.test_light"
assert result.response.response_type == intent.IntentResponseType.ACTION_DONE
assert result.response.intent is not None
assert result.response.intent.slots["name"]["value"] == "test light"
assert result.response.intent.slots["name"]["text"] == "test light"
async def test_expose_flag_automatically_set(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,

View File

@@ -108,7 +108,9 @@ async def test_form_zeroconf_link_local_ignored(hass: HomeAssistant) -> None:
assert result["reason"] == "link_local_address"
async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
async def test_form_zeroconf_ipv4_address(
hass: HomeAssistant, doorbird_api: DoorBird
) -> None:
"""Test we abort and update the ip address from zeroconf with an ipv4 address."""
config_entry = MockConfigEntry(
@@ -118,6 +120,13 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
options={CONF_EVENTS: ["event1", "event2", "event3"]},
)
config_entry.add_to_hass(hass)
# Mock the API to return the correct MAC when validating
doorbird_api.info.return_value = {
"PRIMARY_MAC_ADDR": "1CCAE3AAAAAA",
"WIFI_MAC_ADDR": "1CCAE3BBBBBB",
}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
@@ -136,6 +145,79 @@ async def test_form_zeroconf_ipv4_address(hass: HomeAssistant) -> None:
assert config_entry.data[CONF_HOST] == "4.4.4.4"
async def test_form_zeroconf_ipv4_address_wrong_device(
hass: HomeAssistant, doorbird_api: DoorBird
) -> None:
"""Test we abort when the device MAC doesn't match during zeroconf update."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="1CCAE3AAAAAA",
data=VALID_CONFIG,
options={CONF_EVENTS: ["event1", "event2", "event3"]},
)
config_entry.add_to_hass(hass)
# Mock the API to return a different MAC (wrong device)
doorbird_api.info.return_value = {
"PRIMARY_MAC_ADDR": "1CCAE3DIFFERENT", # Different MAC!
"WIFI_MAC_ADDR": "1CCAE3BBBBBB",
}
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("4.4.4.4"),
ip_addresses=[ip_address("4.4.4.4")],
hostname="mock_hostname",
name="Doorstation - abc123._axis-video._tcp.local.",
port=None,
properties={"macaddress": "1CCAE3AAAAAA"},
type="mock_type",
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "wrong_device"
# Host should not be updated since it's the wrong device
assert config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_form_zeroconf_ipv4_address_cannot_connect(
hass: HomeAssistant, doorbird_api: DoorBird
) -> None:
"""Test we abort when we cannot connect to validate during zeroconf update."""
config_entry = MockConfigEntry(
domain=DOMAIN,
unique_id="1CCAE3AAAAAA",
data=VALID_CONFIG,
options={CONF_EVENTS: ["event1", "event2", "event3"]},
)
config_entry.add_to_hass(hass)
# Mock the API to fail connection (e.g., wrong credentials or network error)
doorbird_api.info.side_effect = mock_unauthorized_exception()
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("4.4.4.4"),
ip_addresses=[ip_address("4.4.4.4")],
hostname="mock_hostname",
name="Doorstation - abc123._axis-video._tcp.local.",
port=None,
properties={"macaddress": "1CCAE3AAAAAA"},
type="mock_type",
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
# Host should not be updated since we couldn't validate
assert config_entry.data[CONF_HOST] == "1.2.3.4"
async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None:
"""Test we abort when we get a non ipv4 address via zeroconf."""

View File

@@ -1,6 +1,7 @@
"""Test Govee light local config flow."""
from errno import EADDRINUSE
from ipaddress import IPv4Address
from unittest.mock import AsyncMock, patch
from govee_local_api import GoveeDevice
@@ -61,17 +62,22 @@ async def test_creating_entry_has_with_devices(
mock_govee_api.devices = _get_devices(mock_govee_api)
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Mock duplicated IPs to ensure that only one GoveeController is started
with patch(
"homeassistant.components.network.async_get_enabled_source_ips",
return_value=[IPv4Address("192.168.1.2"), IPv4Address("192.168.1.2")],
):
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
# Confirmation form
assert result["type"] is FlowResultType.FORM
# Confirmation form
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] is FlowResultType.CREATE_ENTRY
await hass.async_block_till_done()
await hass.async_block_till_done()
mock_govee_api.start.assert_awaited_once()
mock_setup_entry.assert_awaited_once()

View File

@@ -2,6 +2,7 @@
from collections.abc import Callable
import dataclasses
from typing import Any
from unittest import mock
from aiohomekit.controller import TransportType
@@ -11,6 +12,7 @@ from aiohomekit.model.services import Service, ServicesTypes
from aiohomekit.testing import FakeController
import pytest
from homeassistant.components.climate import ATTR_CURRENT_TEMPERATURE
from homeassistant.components.homekit_controller.const import (
DEBOUNCE_COOLDOWN,
DOMAIN,
@@ -439,3 +441,88 @@ async def test_manual_poll_all_chars(
await time_changed(hass, DEBOUNCE_COOLDOWN)
await hass.async_block_till_done()
assert len(mock_get_characteristics.call_args_list[0][0][0]) > 1
async def test_poll_all_on_startup_refreshes_stale_values(
hass: HomeAssistant, hass_storage: dict[str, Any]
) -> None:
"""Test that entities get fresh values on startup instead of stale stored values."""
# Load actual Ecobee accessory fixture
accessories = await setup_accessories_from_file(hass, "ecobee3.json")
# Pre-populate storage with the accessories data (already has stale values)
hass_storage["homekit_controller-entity-map"] = {
"version": 1,
"minor_version": 1,
"key": "homekit_controller-entity-map",
"data": {
"pairings": {
"00:00:00:00:00:00": {
"config_num": 1,
"accessories": [
a.to_accessory_and_service_list() for a in accessories
],
}
}
},
}
# Track what gets polled during setup
polled_chars: list[tuple[int, int]] = []
# Set up the test accessories
fake_controller = await setup_platform(hass)
# Mock get_characteristics to track polling and return fresh temperature
async def mock_get_characteristics(
chars: set[tuple[int, int]], **kwargs: Any
) -> dict[tuple[int, int], dict[str, Any]]:
"""Return fresh temperature value when polled."""
polled_chars.extend(chars)
# Return fresh values for all characteristics
result: dict[tuple[int, int], dict[str, Any]] = {}
for aid, iid in chars:
# Find the characteristic and return appropriate value
for accessory in accessories:
if accessory.aid != aid:
continue
for service in accessory.services:
for char in service.characteristics:
if char.iid != iid:
continue
# Return fresh temperature instead of stale fixture value
if char.type == CharacteristicsTypes.TEMPERATURE_CURRENT:
result[(aid, iid)] = {"value": 22.5} # Fresh value
else:
result[(aid, iid)] = {"value": char.value}
break
return result
# Add the paired device with our mock
await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00")
config_entry = MockConfigEntry(
version=1,
domain="homekit_controller",
entry_id="TestData",
data={"AccessoryPairingID": "00:00:00:00:00:00"},
title="test",
)
config_entry.add_to_hass(hass)
# Get the pairing and patch its get_characteristics
pairing = fake_controller.pairings["00:00:00:00:00:00"]
with mock.patch.object(pairing, "get_characteristics", mock_get_characteristics):
# Set up the config entry (this should trigger poll_all=True)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
# Verify that polling happened during setup (poll_all=True was used)
assert (
len(polled_chars) == 79
) # The Ecobee fixture has exactly 79 readable characteristics
# Check that the climate entity has the fresh temperature (22.5°C) not the stale fixture value (21.8°C)
state = hass.states.get("climate.homew")
assert state is not None
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5

View File

@@ -4,7 +4,7 @@ from ipaddress import ip_address
from unittest.mock import Mock, patch
from aiohue.discovery import URL_NUPNP
from aiohue.errors import LinkButtonNotPressed
from aiohue.errors import AiohueException, LinkButtonNotPressed
import pytest
import voluptuous as vol
@@ -732,3 +732,216 @@ async def test_bridge_connection_failed(
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "cannot_connect"
async def test_bsb003_bridge_discovery(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test a bridge being discovered."""
entry = MockConfigEntry(
domain=const.DOMAIN,
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
unique_id="bsb002_00000",
)
entry.add_to_hass(hass)
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(const.DOMAIN, "bsb002_00000")},
connections={(dr.CONNECTION_NETWORK_MAC, "AA:BB:CC:DD:EE:FF")},
)
create_mock_api_discovery(
aioclient_mock,
[("192.168.1.217", "bsb002_00000"), ("192.168.1.218", "bsb003_00000")],
)
disc_bridge = get_discovered_bridge(
bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True
)
with (
patch(
"homeassistant.components.hue.config_flow.discover_bridge",
return_value=disc_bridge,
),
patch(
"homeassistant.components.hue.config_flow.HueBridgeV2",
autospec=True,
) as mock_bridge,
):
mock_bridge.return_value.fetch_full_state.return_value = {}
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("192.168.1.218"),
ip_addresses=[ip_address("192.168.1.218")],
port=443,
hostname="Philips-hue.local",
type="_hue._tcp.local.",
name="Philips Hue - ABCABC._hue._tcp.local.",
properties={
"bridgeid": "bsb003_00000",
"modelid": "BSB003",
},
),
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "migrated_bridge"
migrated_device = device_registry.async_get(device.id)
assert migrated_device is not None
assert len(migrated_device.identifiers) == 1
assert list(migrated_device.identifiers)[0] == (const.DOMAIN, "bsb003_00000")
# The tests don't add new connection, but that will happen
# outside of the config flow
assert len(migrated_device.connections) == 0
assert entry.data["host"] == "192.168.1.218"
async def test_bsb003_bridge_discovery_old_version(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test a bridge being discovered."""
entry = MockConfigEntry(
domain=const.DOMAIN,
data={"host": "192.168.1.217", "api_version": 1, "api_key": "abc"},
unique_id="bsb002_00000",
)
entry.add_to_hass(hass)
disc_bridge = get_discovered_bridge(
bridge_id="bsb003_00000", host="192.168.1.218", supports_v2=True
)
with patch(
"homeassistant.components.hue.config_flow.discover_bridge",
return_value=disc_bridge,
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("192.168.1.218"),
ip_addresses=[ip_address("192.168.1.218")],
port=443,
hostname="Philips-hue.local",
type="_hue._tcp.local.",
name="Philips Hue - ABCABC._hue._tcp.local.",
properties={
"bridgeid": "bsb003_00000",
"modelid": "BSB003",
},
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "link"
async def test_bsb003_bridge_discovery_same_host(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
device_registry: dr.DeviceRegistry,
) -> None:
"""Test a bridge being discovered."""
entry = MockConfigEntry(
domain=const.DOMAIN,
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
unique_id="bsb002_00000",
)
entry.add_to_hass(hass)
create_mock_api_discovery(
aioclient_mock,
[("192.168.1.217", "bsb003_00000")],
)
disc_bridge = get_discovered_bridge(
bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True
)
with (
patch(
"homeassistant.components.hue.config_flow.discover_bridge",
return_value=disc_bridge,
),
patch(
"homeassistant.components.hue.config_flow.HueBridgeV2",
autospec=True,
),
):
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("192.168.1.217"),
ip_addresses=[ip_address("192.168.1.217")],
port=443,
hostname="Philips-hue.local",
type="_hue._tcp.local.",
name="Philips Hue - ABCABC._hue._tcp.local.",
properties={
"bridgeid": "bsb003_00000",
"modelid": "BSB003",
},
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "link"
@pytest.mark.parametrize("exception", [AiohueException, ClientError])
async def test_bsb003_bridge_discovery_cannot_connect(
hass: HomeAssistant,
aioclient_mock: AiohttpClientMocker,
device_registry: dr.DeviceRegistry,
exception: Exception,
) -> None:
"""Test a bridge being discovered."""
entry = MockConfigEntry(
domain=const.DOMAIN,
data={"host": "192.168.1.217", "api_version": 2, "api_key": "abc"},
unique_id="bsb002_00000",
)
entry.add_to_hass(hass)
create_mock_api_discovery(
aioclient_mock,
[("192.168.1.217", "bsb003_00000")],
)
disc_bridge = get_discovered_bridge(
bridge_id="bsb003_00000", host="192.168.1.217", supports_v2=True
)
with (
patch(
"homeassistant.components.hue.config_flow.discover_bridge",
return_value=disc_bridge,
),
patch(
"homeassistant.components.hue.config_flow.HueBridgeV2",
autospec=True,
) as mock_bridge,
):
mock_bridge.return_value.fetch_full_state.side_effect = exception
result = await hass.config_entries.flow.async_init(
const.DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data=ZeroconfServiceInfo(
ip_address=ip_address("192.168.1.217"),
ip_addresses=[ip_address("192.168.1.217")],
port=443,
hostname="Philips-hue.local",
type="_hue._tcp.local.",
name="Philips Hue - ABCABC._hue._tcp.local.",
properties={
"bridgeid": "bsb003_00000",
"modelid": "BSB003",
},
),
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "link"

View File

@@ -73,6 +73,32 @@ async def test_http_handle_intent(
}
async def test_http_handle_intent_match_failure(
hass: HomeAssistant, hass_client: ClientSessionGenerator, hass_admin_user: MockUser
) -> None:
"""Test handle intent match failure via HTTP API."""
assert await async_setup_component(hass, "intent", {})
hass.states.async_set(
"cover.garage_door_1", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"}
)
hass.states.async_set(
"cover.garage_door_2", "closed", {ATTR_FRIENDLY_NAME: "Garage Door"}
)
async_mock_service(hass, "cover", SERVICE_OPEN_COVER)
client = await hass_client()
resp = await client.post(
"/api/intent/handle",
json={"name": "HassTurnOn", "data": {"name": "Garage Door"}},
)
assert resp.status == 200
data = await resp.json()
assert "DUPLICATE_NAME" in data["speech"]["plain"]["speech"]
async def test_cover_intents_loading(hass: HomeAssistant) -> None:
"""Test Cover Intents Loading."""
assert await async_setup_component(hass, "intent", {})

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