Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion homeassistant/components/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@
ATTR_INTERNAL_URL = "internal_url"
ATTR_LOCATION_NAME = "location_name"
ATTR_INSTALLATION_TYPE = "installation_type"
ATTR_REQUIRES_API_PASSWORD = "requires_api_password"
ATTR_UUID = "uuid"
ATTR_VERSION = "version"

Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/assist_satellite/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from pathlib import Path
from typing import Any

from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.util import (
PUNCTUATION_END,
PUNCTUATION_END_WORD,
Expand Down Expand Up @@ -164,6 +166,7 @@ async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
],
Expand Down Expand Up @@ -212,6 +215,17 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value


def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err

return value


def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
Expand Down
19 changes: 18 additions & 1 deletion homeassistant/components/conversation/trigger.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from collections.abc import Awaitable, Callable
from typing import Any

from hassil.parse_expression import parse_sentence
from hassil.parser import ParseError
from hassil.recognize import RecognizeResult
from hassil.util import (
PUNCTUATION_END,
Expand Down Expand Up @@ -42,6 +44,17 @@ def has_no_punctuation(value: list[str]) -> list[str]:
return value


def is_valid_sentence(value: list[str]) -> list[str]:
"""Validate result can be parsed by hassil."""
for sentence in value:
try:
parse_sentence(sentence)
except ParseError as err:
raise vol.Invalid(f"invalid sentence: {err}") from err

return value


def has_one_non_empty_item(value: list[str]) -> list[str]:
"""Validate result has at least one item."""
if len(value) < 1:
Expand All @@ -58,7 +71,11 @@ def has_one_non_empty_item(value: list[str]) -> list[str]:
{
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(CONF_COMMAND): vol.All(
cv.ensure_list, [cv.string], has_one_non_empty_item, has_no_punctuation
cv.ensure_list,
[cv.string],
has_one_non_empty_item,
has_no_punctuation,
is_valid_sentence,
),
}
)
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/esphome/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ class RuntimeEntryData:
)
loaded_platforms: set[Platform] = field(default_factory=set)
platform_load_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
# Set once the first connection has finished scanner setup or teardown.
first_connect_done: asyncio.Event = field(default_factory=asyncio.Event)
_storage_contents: StoreData | None = None
_pending_storage: Callable[[], StoreData] | None = None
assist_pipeline_update_callbacks: list[CALLBACK_TYPE] = field(default_factory=list)
Expand Down
23 changes: 22 additions & 1 deletion homeassistant/components/esphome/manager.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Manager for esphome devices."""

import asyncio
import base64
from functools import partial
import logging
import secrets
import struct
from typing import TYPE_CHECKING, Any, NamedTuple
from typing import TYPE_CHECKING, Any, Final, NamedTuple

from aioesphomeapi import (
APIClient,
Expand Down Expand Up @@ -106,6 +107,9 @@

_LOGGER = logging.getLogger(__name__)

# Max time to wait at startup for a BLE proxy to register its scanner.
STARTUP_SCANNER_WAIT: Final = 3.0

LOG_LEVEL_TO_LOGGER = {
LogLevel.LOG_LEVEL_NONE: logging.DEBUG,
LogLevel.LOG_LEVEL_ERROR: logging.ERROR,
Expand Down Expand Up @@ -677,6 +681,8 @@ async def _on_connect(self) -> None:
hass, device_info.bluetooth_mac_address or device_info.mac_address
)

entry_data.first_connect_done.set()

if device_info.voice_assistant_feature_flags_compat(api_version) and (
Platform.ASSIST_SATELLITE not in entry_data.loaded_platforms
):
Expand Down Expand Up @@ -988,6 +994,21 @@ async def async_start(self) -> None:

await reconnect_logic.start()

# Wait for a cached BLE proxy to register its scanner before finishing setup.
if (
device_info := entry_data.device_info
) is not None and device_info.bluetooth_proxy_feature_flags_compat(
entry_data.api_version
):
try:
async with asyncio.timeout(STARTUP_SCANNER_WAIT):
await entry_data.first_connect_done.wait()
except TimeoutError:
_LOGGER.debug(
"%s: Timed out waiting for Bluetooth scanner to register",
self.host,
)


@callback
def _async_setup_device_registry(
Expand Down
52 changes: 3 additions & 49 deletions homeassistant/components/landisgyr_heat_meter/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,19 @@
import ultraheat_api
import voluptuous as vol

from homeassistant.components import usb
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.selector import SerialPortSelector

from .const import DOMAIN, ULTRAHEAT_TIMEOUT

_LOGGER = logging.getLogger(__name__)

CONF_MANUAL_PATH = "Enter Manually"

STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE): str,
vol.Required(CONF_DEVICE): SerialPortSelector(),
}
)

Expand All @@ -39,9 +37,6 @@ async def async_step_user(
errors = {}

if user_input is not None:
if user_input[CONF_DEVICE] == CONF_MANUAL_PATH:
return await self.async_step_setup_serial_manual_path()

dev_path = user_input[CONF_DEVICE]
_LOGGER.debug("Using this path : %s", dev_path)

Expand All @@ -50,30 +45,8 @@ async def async_step_user(
except CannotConnect:
errors["base"] = "cannot_connect"

ports = await get_usb_ports(self.hass)
ports[CONF_MANUAL_PATH] = CONF_MANUAL_PATH

schema = vol.Schema({vol.Required(CONF_DEVICE): vol.In(ports)})
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)

async def async_step_setup_serial_manual_path(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Set path manually."""
errors = {}

if user_input is not None:
dev_path = user_input[CONF_DEVICE]
try:
return await self.validate_and_create_entry(dev_path)
except CannotConnect:
errors["base"] = "cannot_connect"

schema = vol.Schema({vol.Required(CONF_DEVICE): str})
return self.async_show_form(
step_id="setup_serial_manual_path",
data_schema=schema,
errors=errors,
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)

async def validate_and_create_entry(self, dev_path):
Expand Down Expand Up @@ -111,24 +84,5 @@ async def validate_ultraheat(self, port: str) -> tuple[str, str]:
return data.model, data.device_number


async def get_usb_ports(hass: HomeAssistant) -> dict[str, str]:
"""Return a dict of USB ports and their friendly names."""
ports = await usb.async_scan_serial_ports(hass)
port_descriptions = {}
for port in ports:
if isinstance(port, usb.USBDevice):
human_name = usb.human_readable_device_name(
port.device,
port.serial_number,
port.manufacturer,
port.description,
port.vid,
port.pid,
)
port_descriptions[port.device] = human_name

return port_descriptions


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
2 changes: 1 addition & 1 deletion homeassistant/components/landisgyr_heat_meter/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@

DOMAIN = "landisgyr_heat_meter"

ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time
ULTRAHEAT_TIMEOUT = 60 # reading the IR port can take some time
POLLING_INTERVAL = timedelta(days=1) # Polling is only daily to prevent battery drain.
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/landisgyr_heat_meter",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["ultraheat-api==0.6.0"]
"requirements": ["ultraheat-api==0.6.1"]
}
5 changes: 0 additions & 5 deletions homeassistant/components/landisgyr_heat_meter/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"setup_serial_manual_path": {
"data": {
"device": "[%key:common::config_flow::data::usb_path%]"
}
},
"user": {
"data": {
"device": "Select device"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/ollama/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/ollama",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["ollama==0.5.1"]
"requirements": ["ollama==0.6.2"]
}
4 changes: 2 additions & 2 deletions homeassistant/components/plugwise/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,9 @@ def extra_restore_state_data(self) -> PlugwiseClimateExtraStoredData:
)

@property
def current_temperature(self) -> float:
def current_temperature(self) -> float | None:
"""Return the current temperature."""
return self.device["sensors"]["temperature"]
return self.device["sensors"].get("temperature")

@property
def target_temperature(self) -> float:
Expand Down
20 changes: 20 additions & 0 deletions homeassistant/components/shelly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""The Shelly integration."""

import asyncio
from functools import partial
from typing import Final

Expand Down Expand Up @@ -73,6 +74,7 @@
get_http_port,
get_rpc_scripts_event_types,
get_ws_context,
is_rpc_ble_scanner_supported,
remove_empty_sub_devices,
remove_stale_blu_trv_devices,
)
Expand Down Expand Up @@ -114,6 +116,9 @@
)
CONFIG_SCHEMA: Final = vol.Schema({DOMAIN: COAP_SCHEMA}, extra=vol.ALLOW_EXTRA)

# Max time to wait at startup for a BLE proxy to register its scanner.
STARTUP_SCANNER_WAIT: Final = 3.0


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Shelly component."""
Expand Down Expand Up @@ -365,6 +370,21 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)

runtime_data.rpc = ShellyRpcCoordinator(hass, entry, device)
runtime_data.rpc.async_setup()

if (
is_rpc_ble_scanner_supported(entry)
and entry.options.get(CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED)
!= BLEScannerMode.DISABLED
):
# Wait for the proxy to register its scanner before finishing setup.
try:
async with asyncio.timeout(STARTUP_SCANNER_WAIT):
await runtime_data.rpc.ble_scanner_setup_done.wait()
except TimeoutError:
LOGGER.debug(
"%s: Timed out waiting for BLE scanner to register", entry.title
)

runtime_data.rpc_poll = ShellyRpcPollingCoordinator(hass, entry, device)
await hass.config_entries.async_forward_entry_setups(
entry, runtime_data.platforms
Expand Down
43 changes: 24 additions & 19 deletions homeassistant/components/shelly/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,8 @@ def __init__(
super().__init__(hass, entry, device, update_interval)

self.connected = False
# Set once BLE scanner setup has been attempted after connecting.
self.ble_scanner_setup_done = asyncio.Event()
self._disconnected_callbacks: list[CALLBACK_TYPE] = []
self._connection_lock = asyncio.Lock()
self._event_listeners: list[Callable[[dict[str, Any]], None]] = []
Expand Down Expand Up @@ -759,27 +761,30 @@ async def _async_setup_outbound_websocket(self) -> None:

async def _async_connect_ble_scanner(self) -> None:
"""Connect BLE scanner."""
ble_scanner_mode = self.config_entry.options.get(
CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED
)
if ble_scanner_mode == BLEScannerMode.DISABLED and self.connected:
await async_stop_scanner(self.device)
async_remove_scanner(self.hass, self.bluetooth_source)
return
if await async_ensure_ble_enabled(self.device):
# BLE enable required a reboot, don't bother connecting
# the scanner since it will be disconnected anyway
LOGGER.debug(
"Device %s BLE enable required a reboot, skipping scanner connect",
self.name,
try:
ble_scanner_mode = self.config_entry.options.get(
CONF_BLE_SCANNER_MODE, BLEScannerMode.DISABLED
)
return
assert self.device_id is not None
self._disconnected_callbacks.append(
await async_connect_scanner(
self.hass, self, ble_scanner_mode, self.device_id
if ble_scanner_mode == BLEScannerMode.DISABLED and self.connected:
await async_stop_scanner(self.device)
async_remove_scanner(self.hass, self.bluetooth_source)
return
if await async_ensure_ble_enabled(self.device):
# BLE enable required a reboot, don't bother connecting
# the scanner since it will be disconnected anyway
LOGGER.debug(
"Device %s BLE enable required a reboot, skipping scanner connect",
self.name,
)
return
assert self.device_id is not None
self._disconnected_callbacks.append(
await async_connect_scanner(
self.hass, self, ble_scanner_mode, self.device_id
)
)
)
finally:
self.ble_scanner_setup_done.set()

@callback
def _async_handle_rpc_device_online(self) -> None:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/victron_gx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"quality_scale": "platinum",
"requirements": ["victron-mqtt==2026.6.1"],
"requirements": ["victron-mqtt==2026.6.1.1"],
"ssdp": [
{
"X_MqttOnLan": "1",
Expand Down
Loading
Loading