mirror of
https://github.com/home-assistant/core.git
synced 2025-09-19 18:09:48 +00:00
Compare commits
67 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
3499ed7a98 | ||
![]() |
2c809d5903 | ||
![]() |
40988198f3 | ||
![]() |
ab5d1d27f1 | ||
![]() |
1c10b85fed | ||
![]() |
91a7db08ff | ||
![]() |
a764d54123 | ||
![]() |
dc09e33556 | ||
![]() |
14173bd9ec | ||
![]() |
d2e7537629 | ||
![]() |
9a165a64fe | ||
![]() |
9c749a6abc | ||
![]() |
2e33222c71 | ||
![]() |
ab1c2c4f70 | ||
![]() |
529219ae69 | ||
![]() |
d6ce71fa61 | ||
![]() |
e5b67d513a | ||
![]() |
a547179f66 | ||
![]() |
8c61788a7d | ||
![]() |
6b934d94db | ||
![]() |
d30ad82774 | ||
![]() |
4618b33e93 | ||
![]() |
d6299094db | ||
![]() |
087d9d30c0 | ||
![]() |
f07890cf5c | ||
![]() |
e5b78cc481 | ||
![]() |
12b409d8e1 | ||
![]() |
def5408db8 | ||
![]() |
f105b45ee2 | ||
![]() |
9d904c30a7 | ||
![]() |
99b047939f | ||
![]() |
3a615908ee | ||
![]() |
baff541f46 | ||
![]() |
6d8c35cfe9 | ||
![]() |
b8d9883e74 | ||
![]() |
c3c65af450 | ||
![]() |
3af8616764 | ||
![]() |
64ec4609c5 | ||
![]() |
c78bc26b83 | ||
![]() |
0c093646c9 | ||
![]() |
1b27acdde0 | ||
![]() |
9dafc0e02f | ||
![]() |
0091dafcb0 | ||
![]() |
b387acffb7 | ||
![]() |
36b3133fa2 | ||
![]() |
fe01e96012 | ||
![]() |
0b56ec16ed | ||
![]() |
ca79f4c963 | ||
![]() |
9a43f2776d | ||
![]() |
0cda883b56 | ||
![]() |
ae58e633f0 | ||
![]() |
06480bfd9d | ||
![]() |
625f586945 | ||
![]() |
7dbeaa475d | ||
![]() |
dff3d5f8af | ||
![]() |
89c335919a | ||
![]() |
2bb4573357 | ||
![]() |
7037ce989c | ||
![]() |
bfdd2053ba | ||
![]() |
fcc3f92f8c | ||
![]() |
8710267d53 | ||
![]() |
85b6adcc9a | ||
![]() |
beec6e86e0 | ||
![]() |
3dacffaaf9 | ||
![]() |
d90f2a1de1 | ||
![]() |
b6c9217429 | ||
![]() |
7fc8da6769 |
4
CODEOWNERS
generated
4
CODEOWNERS
generated
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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(
|
||||
|
@@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
|
@@ -6,6 +6,7 @@ _LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
CONF_SITE = "site"
|
||||
|
||||
DEFAULT_DOMAIN = "com"
|
||||
COUNTRY_DOMAINS = {
|
||||
|
@@ -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"
|
||||
]
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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 = {
|
||||
|
@@ -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})
|
||||
|
||||
|
@@ -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})",
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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%]",
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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",
|
||||
|
@@ -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:
|
||||
|
@@ -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(
|
||||
|
@@ -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."""
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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))
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
@@ -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),
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
),
|
||||
|
@@ -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] = {}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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)
|
||||
|
@@ -54,6 +54,6 @@
|
||||
"requirements": [
|
||||
"aiolifx==1.2.1",
|
||||
"aiolifx-effects==0.3.2",
|
||||
"aiolifx-themes==0.6.4"
|
||||
"aiolifx-themes==1.0.2"
|
||||
]
|
||||
}
|
||||
|
@@ -57,7 +57,8 @@
|
||||
},
|
||||
"extra_fields": {
|
||||
"brightness_pct": "Brightness",
|
||||
"flash": "Flash"
|
||||
"flash": "Flash",
|
||||
"for": "[%key:common::device_automation::extra_fields::for%]"
|
||||
}
|
||||
},
|
||||
"entity_component": {
|
||||
|
@@ -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,
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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"]
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aionfty"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiontfy==0.5.4"]
|
||||
"requirements": ["aiontfy==0.5.5"]
|
||||
}
|
||||
|
@@ -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()
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.5.1"]
|
||||
"requirements": ["ohme==1.5.2"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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": {
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus", "pysmarty2"],
|
||||
"requirements": ["pysmarty2==0.10.2"]
|
||||
"requirements": ["pysmarty2==0.10.3"]
|
||||
}
|
||||
|
@@ -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:
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -116,6 +116,7 @@ CONTENT_TYPE_TO_CHILD_TYPE: dict[
|
||||
MediaType.APPS: MediaType.APP,
|
||||
MediaType.APP: MediaType.TRACK,
|
||||
"favorite": None,
|
||||
"track": MediaType.TRACK,
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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()
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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": {
|
||||
|
@@ -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}
|
||||
|
@@ -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:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "velux",
|
||||
"name": "Velux",
|
||||
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio"],
|
||||
"codeowners": ["@Julius2342", "@DeerMaximum", "@pawlizio", "@wollew"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
{
|
||||
|
@@ -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(
|
||||
|
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiovodafone"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiovodafone==0.10.0"]
|
||||
"requirements": ["aiovodafone==1.2.1"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
@@ -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):
|
||||
|
@@ -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
|
||||
|
@@ -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
2
requirements.txt
generated
@@ -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
42
requirements_all.txt
generated
@@ -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
|
||||
|
42
requirements_test_all.txt
generated
42
requirements_test_all.txt
generated
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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()
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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,
|
||||
|
@@ -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",
|
||||
},
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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({
|
||||
|
@@ -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,
|
||||
|
@@ -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."""
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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"
|
||||
|
@@ -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
Reference in New Issue
Block a user