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
2 changes: 1 addition & 1 deletion homeassistant/components/adax/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.1.5"]
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
}
13 changes: 11 additions & 2 deletions homeassistant/components/esphome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -542,7 +542,16 @@ async def _async_create_entry(self) -> ConfigFlowResult:

# Check if Z-Wave capabilities are present and start discovery flow
next_flow_id: str | None = None
if self._device_info.zwave_proxy_feature_flags:
# If the zwave_home_id is not set, we don't know if it's a fresh
# adapter, or the cable is just unplugged. So only start
# the zwave_js config flow automatically if there is a
# zwave_home_id present. If it's a fresh adapter, the manager
# will handle starting the flow once it gets the home id changed
# request from the ESPHome device.
if (
self._device_info.zwave_proxy_feature_flags
and self._device_info.zwave_home_id
):
assert self._connected_address is not None
assert self._port is not None

Expand All @@ -559,7 +568,7 @@ async def _async_create_entry(self) -> ConfigFlowResult:
},
data=ESPHomeServiceInfo(
name=self._device_info.name,
zwave_home_id=self._device_info.zwave_home_id or None,
zwave_home_id=self._device_info.zwave_home_id,
ip_address=self._connected_address,
port=self._port,
noise_psk=self._noise_psk,
Expand Down
19 changes: 18 additions & 1 deletion homeassistant/components/esphome/entry_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -491,13 +491,30 @@ def async_on_connect(

assert self.client.connected_address

# If the device does not have a zwave_home_id, it means
# either the Z-Wave controller has never been connected
# to the ESPHome device, or the Z-Wave controller has
# never been provisioned with a home ID (brand new).
# Since we cannot tell the difference, and it could
# just be the cable is unplugged we only
# automatically start the flow if we have a home ID.
if not device_info.zwave_home_id:
return

self.async_create_zwave_js_flow(hass, device_info, device_info.zwave_home_id)

def async_create_zwave_js_flow(
self, hass: HomeAssistant, device_info: DeviceInfo, zwave_home_id: int
) -> None:
"""Create a zwave_js config flow for a Z-Wave JS Proxy device."""
assert self.client.connected_address is not None
discovery_flow.async_create_flow(
hass,
"zwave_js",
{"source": config_entries.SOURCE_ESPHOME},
ESPHomeServiceInfo(
name=device_info.name,
zwave_home_id=device_info.zwave_home_id or None,
zwave_home_id=zwave_home_id,
ip_address=self.client.connected_address,
port=self.client.port,
noise_psk=self.client.noise_psk,
Expand Down
29 changes: 29 additions & 0 deletions homeassistant/components/esphome/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from functools import partial
import logging
import secrets
import struct
from typing import TYPE_CHECKING, Any, NamedTuple

from aioesphomeapi import (
Expand All @@ -22,6 +23,8 @@
RequiresEncryptionAPIError,
UserService,
UserServiceArgType,
ZWaveProxyRequest,
ZWaveProxyRequestType,
parse_log_message,
)
from awesomeversion import AwesomeVersion
Expand Down Expand Up @@ -84,6 +87,8 @@
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData

DEVICE_CONFLICT_ISSUE_FORMAT = "device_conflict-{}"
UNPACK_UINT32_BE = struct.Struct(">I").unpack_from


if TYPE_CHECKING:
from aioesphomeapi.api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] # noqa: I001
Expand Down Expand Up @@ -557,6 +562,11 @@ async def _on_connect(self) -> None:
)
entry_data.loaded_platforms.add(Platform.ASSIST_SATELLITE)

if device_info.zwave_proxy_feature_flags:
entry_data.disconnect_callbacks.add(
cli.subscribe_zwave_proxy_request(self._async_zwave_proxy_request)
)

cli.subscribe_home_assistant_states_and_services(
on_state=entry_data.async_update_state,
on_service_call=self.async_on_service_call,
Expand All @@ -568,6 +578,25 @@ async def _on_connect(self) -> None:
_async_check_firmware_version(hass, device_info, api_version)
_async_check_using_api_password(hass, device_info, bool(self.password))

def _async_zwave_proxy_request(self, request: ZWaveProxyRequest) -> None:
"""Handle a request to create a zwave_js config flow."""
if request.type != ZWaveProxyRequestType.HOME_ID_CHANGE:
return
# ESPHome will send a home id change on every connection
# if the Z-Wave controller is connected to the ESPHome device
# so we know for sure that the Z-Wave controller is connected
# when we get the message. This makes it safe to start
# the zwave_js config flow automatically even if the zwave_home_id
# is 0 (not yet provisioned) as we know for sure the controller
# is connected to the ESPHome device and do not have to guess
# if it's a broken connection or Z-Wave controller or a not
# yet provisioned controller.
zwave_home_id: int = UNPACK_UINT32_BE(request.data[0:4])[0]
assert self.entry_data.device_info is not None
self.entry_data.async_create_zwave_js_flow(
self.hass, self.entry_data.device_info, zwave_home_id
)

async def on_disconnect(self, expected_disconnect: bool) -> None:
"""Run disconnect callbacks on API disconnect."""
entry_data = self.entry_data
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/feedreader/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"codeowners": ["@mib1185"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/feedreader",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["feedparser", "sgmllib3k"],
"requirements": ["feedparser==6.0.12"]
Expand Down
12 changes: 12 additions & 0 deletions homeassistant/components/fritz/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import asyncio
from collections.abc import Callable, Mapping
from dataclasses import dataclass, field
from datetime import datetime, timedelta
Expand All @@ -16,6 +17,7 @@
FritzConnectionException,
FritzSecurityError,
)
from fritzconnection.lib.fritzcall import FritzCall
from fritzconnection.lib.fritzhosts import FritzHosts
from fritzconnection.lib.fritzstatus import FritzStatus
from fritzconnection.lib.fritzwlan import FritzGuestWLAN
Expand Down Expand Up @@ -120,6 +122,7 @@ def __init__(
self.fritz_guest_wifi: FritzGuestWLAN = None
self.fritz_hosts: FritzHosts = None
self.fritz_status: FritzStatus = None
self.fritz_call: FritzCall = None
self.host = host
self.mesh_role = MeshRoles.NONE
self.mesh_wifi_uplink = False
Expand Down Expand Up @@ -183,6 +186,7 @@ def setup(self) -> None:
self.fritz_hosts = FritzHosts(fc=self.connection)
self.fritz_guest_wifi = FritzGuestWLAN(fc=self.connection)
self.fritz_status = FritzStatus(fc=self.connection)
self.fritz_call = FritzCall(fc=self.connection)
info = self.fritz_status.get_device_info()

_LOGGER.debug(
Expand Down Expand Up @@ -617,6 +621,14 @@ async def async_trigger_set_guest_password(
self.fritz_guest_wifi.set_password, password, length
)

async def async_trigger_dial(self, number: str, max_ring_seconds: int) -> None:
"""Trigger service to dial a number."""
try:
await self.hass.async_add_executor_job(self.fritz_call.dial, number)
await asyncio.sleep(max_ring_seconds)
finally:
await self.hass.async_add_executor_job(self.fritz_call.hangup)

async def async_trigger_cleanup(self) -> None:
"""Trigger device trackers cleanup."""
_LOGGER.debug("Device tracker cleanup triggered")
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/fritz/icons.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@
},
"set_guest_wifi_password": {
"service": "mdi:form-textbox-password"
},
"dial": {
"service": "mdi:phone-dial"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/components/fritz/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/fritz",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["fritzconnection"],
"requirements": ["fritzconnection[qr]==1.15.0", "xmltodict==0.13.0"],
Expand Down
50 changes: 50 additions & 0 deletions homeassistant/components/fritz/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from fritzconnection.core.exceptions import (
FritzActionError,
FritzActionFailedError,
FritzConnectionException,
FritzServiceError,
)
Expand All @@ -27,6 +28,14 @@
vol.Optional("length"): vol.Range(min=8, max=63),
}
)
SERVICE_DIAL = "dial"
SERVICE_SCHEMA_DIAL = vol.Schema(
{
vol.Required("device_id"): str,
vol.Required("number"): str,
vol.Required("max_ring_seconds"): vol.Range(min=1, max=300),
}
)


async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
Expand Down Expand Up @@ -65,6 +74,46 @@ async def _async_set_guest_wifi_password(service_call: ServiceCall) -> None:
) from ex


async def _async_dial(service_call: ServiceCall) -> None:
"""Call Fritz dial service."""
target_entry_ids = await async_extract_config_entry_ids(service_call)
target_entries: list[FritzConfigEntry] = [
loaded_entry
for loaded_entry in service_call.hass.config_entries.async_loaded_entries(
DOMAIN
)
if loaded_entry.entry_id in target_entry_ids
]

if not target_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_found",
translation_placeholders={"service": service_call.service},
)

for target_entry in target_entries:
_LOGGER.debug("Executing service %s", service_call.service)
avm_wrapper = target_entry.runtime_data
try:
await avm_wrapper.async_trigger_dial(
service_call.data["number"],
max_ring_seconds=service_call.data["max_ring_seconds"],
)
except (FritzServiceError, FritzActionError) as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_parameter_unknown"
) from ex
except FritzActionFailedError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_dial_failed"
) from ex
except FritzConnectionException as ex:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_not_supported"
) from ex


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up services for Fritz integration."""
Expand All @@ -75,3 +124,4 @@ def async_setup_services(hass: HomeAssistant) -> None:
_async_set_guest_wifi_password,
SERVICE_SCHEMA_SET_GUEST_WIFI_PW,
)
hass.services.async_register(DOMAIN, SERVICE_DIAL, _async_dial, SERVICE_SCHEMA_DIAL)
21 changes: 21 additions & 0 deletions homeassistant/components/fritz/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,24 @@ set_guest_wifi_password:
number:
min: 8
max: 63
dial:
fields:
device_id:
required: true
selector:
device:
integration: fritz
entity:
device_class: connectivity
number:
required: true
selector:
text:
max_ring_seconds:
default: 15
required: true
selector:
number:
min: 1
max: 300
unit_of_measurement: seconds
21 changes: 21 additions & 0 deletions homeassistant/components/fritz/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -198,12 +198,33 @@
"description": "Length of the new password. It will be auto-generated if no password is set."
}
}
},
"dial": {
"name": "Dial a phone number",
"description": "Makes the FRITZ!Box dial a phone number.",
"fields": {
"device_id": {
"name": "FRITZ!Box device",
"description": "Select the FRITZ!Box to dial from."
},
"number": {
"name": "Phone number",
"description": "The phone number to dial."
},
"max_ring_seconds": {
"name": "Maximum ring duration",
"description": "The maximum number of seconds to ring after dialing."
}
}
}
},
"exceptions": {
"config_entry_not_found": {
"message": "Failed to perform action \"{service}\". Config entry for target not found"
},
"service_dial_failed": {
"message": "Failed to dial, check if the click to dial service of the FRITZ!Box is activated"
},
"service_parameter_unknown": {
"message": "Action or parameter unknown"
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/knx/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"requirements": [
"xknx==3.10.0",
"xknxproject==3.8.2",
"knx-frontend==2025.10.9.185845"
"knx-frontend==2025.10.17.202411"
],
"single_config_entry": true
}
Loading
Loading