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
10 changes: 5 additions & 5 deletions .github/workflows/builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ jobs:
echo "${{ github.sha }};${{ github.ref }};${{ github.event_name }};${{ github.actor }}" > rootfs/OFFICIAL_IMAGE

- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down Expand Up @@ -256,7 +256,7 @@ jobs:
fi

- name: Login to GitHub Container Registry
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down Expand Up @@ -330,14 +330,14 @@ jobs:

- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Login to GitHub Container Registry
if: matrix.registry == 'ghcr.io/home-assistant'
uses: docker/login-action@v3.4.0
uses: docker/login-action@v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down Expand Up @@ -502,7 +502,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

- name: Login to GitHub Container Registry
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/detect-duplicate-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@v1.2.4
uses: actions/ai-inference@v1.2.7
with:
model: openai/gpt-4o
system-prompt: |
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/detect-non-english-issues.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI
id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@v1.2.4
uses: actions/ai-inference@v1.2.7
with:
model: openai/gpt-4o-mini
system-prompt: |
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/downloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# If path is relative, we assume relative to Home Assistant config dir
if not os.path.isabs(download_path):
download_path = hass.config.path(download_path)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_DOWNLOAD_DIR: download_path}
)

if not await hass.async_add_executor_job(os.path.isdir, download_path):
_LOGGER.error(
Expand Down
38 changes: 24 additions & 14 deletions homeassistant/components/downloader/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import voluptuous as vol

from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
Expand All @@ -34,24 +35,33 @@ def download_file(service: ServiceCall) -> None:

entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
url: str = service.data[ATTR_URL]
subdir: str | None = service.data.get(ATTR_SUBDIR)
target_filename: str | None = service.data.get(ATTR_FILENAME)
overwrite: bool = service.data[ATTR_OVERWRITE]

if subdir:
# Check the path
try:
raise_if_invalid_path(subdir)
except ValueError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_invalid",
translation_placeholders={"subdir": subdir},
) from err
if os.path.isabs(subdir):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="subdir_not_relative",
translation_placeholders={"subdir": subdir},
)

def do_download() -> None:
"""Download the file."""
final_path = None
filename = target_filename
try:
url = service.data[ATTR_URL]

subdir = service.data.get(ATTR_SUBDIR)

filename = service.data.get(ATTR_FILENAME)

overwrite = service.data.get(ATTR_OVERWRITE)

if subdir:
# Check the path
raise_if_invalid_path(subdir)

final_path = None

req = requests.get(url, stream=True, timeout=10)

if req.status_code != HTTPStatus.OK:
Expand Down
8 changes: 8 additions & 0 deletions homeassistant/components/downloader/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]"
}
},
"exceptions": {
"subdir_invalid": {
"message": "Invalid subdirectory, got: {subdir}"
},
"subdir_not_relative": {
"message": "Subdirectory must be relative, got: {subdir}"
}
},
"services": {
"download_file": {
"name": "Download file",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/frontend/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250731.0"]
"requirements": ["home-assistant-frontend==20250805.0"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"set_options": {
"data": {
Expand Down
60 changes: 57 additions & 3 deletions homeassistant/components/husqvarna_automower/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import asyncio
from collections.abc import Callable
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import override

Expand All @@ -14,7 +14,7 @@
HusqvarnaTimeoutError,
HusqvarnaWSServerHandshakeError,
)
from aioautomower.model import MowerDictionary
from aioautomower.model import MowerDictionary, MowerStates
from aioautomower.session import AutomowerSession

from homeassistant.config_entries import ConfigEntry
Expand All @@ -29,7 +29,9 @@
MAX_WS_RECONNECT_TIME = 600
SCAN_INTERVAL = timedelta(minutes=8)
DEFAULT_RECONNECT_TIME = 2 # Define a default reconnect time

PONG_TIMEOUT = timedelta(seconds=90)
PING_INTERVAL = timedelta(seconds=10)
PING_TIMEOUT = timedelta(seconds=5)
type AutomowerConfigEntry = ConfigEntry[AutomowerDataUpdateCoordinator]


Expand Down Expand Up @@ -58,6 +60,9 @@ def __init__(
self.new_devices_callbacks: list[Callable[[set[str]], None]] = []
self.new_zones_callbacks: list[Callable[[str, set[str]], None]] = []
self.new_areas_callbacks: list[Callable[[str, set[int]], None]] = []
self.pong: datetime | None = None
self.websocket_alive: bool = False
self._watchdog_task: asyncio.Task | None = None

@override
@callback
Expand All @@ -71,6 +76,18 @@ async def _async_update_data(self) -> MowerDictionary:
await self.api.connect()
self.api.register_data_callback(self.handle_websocket_updates)
self.ws_connected = True

def start_watchdog() -> None:
if self._watchdog_task is not None and not self._watchdog_task.done():
_LOGGER.debug("Cancelling previous watchdog task")
self._watchdog_task.cancel()
self._watchdog_task = self.config_entry.async_create_background_task(
self.hass,
self._pong_watchdog(),
"websocket_watchdog",
)

self.api.register_ws_ready_callback(start_watchdog)
try:
data = await self.api.get_status()
except ApiError as err:
Expand All @@ -93,6 +110,19 @@ def _on_data_update(self) -> None:
mower_data.capabilities.work_areas for mower_data in self.data.values()
):
self._async_add_remove_work_areas()
if (
not self._should_poll()
and self.update_interval is not None
and self.websocket_alive
):
_LOGGER.debug("All mowers inactive and websocket alive: stop polling")
self.update_interval = None
if self.update_interval is None and self._should_poll():
_LOGGER.debug(
"Polling re-enabled via WebSocket: at least one mower active"
)
self.update_interval = SCAN_INTERVAL
self.hass.async_create_task(self.async_request_refresh())

@callback
def handle_websocket_updates(self, ws_data: MowerDictionary) -> None:
Expand Down Expand Up @@ -161,6 +191,30 @@ async def client_listen(
"reconnect_task",
)

def _should_poll(self) -> bool:
"""Return True if at least one mower is connected and at least one is not OFF."""
return any(mower.metadata.connected for mower in self.data.values()) and any(
mower.mower.state != MowerStates.OFF for mower in self.data.values()
)

async def _pong_watchdog(self) -> None:
_LOGGER.debug("Watchdog started")
try:
while True:
_LOGGER.debug("Sending ping")
self.websocket_alive = await self.api.send_empty_message()
_LOGGER.debug("Ping result: %s", self.websocket_alive)

await asyncio.sleep(60)
_LOGGER.debug("Websocket alive %s", self.websocket_alive)
if not self.websocket_alive:
_LOGGER.debug("No pong received → restart polling")
if self.update_interval is None:
self.update_interval = SCAN_INTERVAL
await self.async_request_refresh()
except asyncio.CancelledError:
_LOGGER.debug("Watchdog cancelled")

def _async_add_remove_devices(self) -> None:
"""Add new devices and remove orphaned devices from the registry."""
current_devices = set(self.data)
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/mealie/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ async def async_create_todo_item(self, item: TodoItem) -> None:
list_id=self._shopping_list_id,
note=item.summary.strip() if item.summary else item.summary,
position=position,
quantity=0.0,
)
try:
await self.coordinator.client.add_shopping_item(new_shopping_item)
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/ollama/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"set_options": {
"data": {
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/open_router/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@
}
},
"initiate_flow": {
"user": "Add Generate data with AI service"
"user": "Add AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
Expand Down
6 changes: 3 additions & 3 deletions homeassistant/components/openai_conversation/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@
},
"ai_task_data": {
"initiate_flow": {
"user": "Add Generate data with AI service",
"reconfigure": "Reconfigure Generate data with AI service"
"user": "Add AI task",
"reconfigure": "Reconfigure AI task"
},
"entry_type": "Generate data with AI service",
"entry_type": "AI task",
"step": {
"init": {
"data": {
Expand Down
4 changes: 1 addition & 3 deletions homeassistant/components/reolink/media_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,7 @@ async def _async_generate_camera_files(
file_name = f"{file.start_time.time()} {file.duration}"
if file.triggers != file.triggers.NONE:
file_name += " " + " ".join(
str(trigger.name).title()
for trigger in file.triggers
if trigger != trigger.NONE
str(trigger.name).title() for trigger in file.triggers
)

children.append(
Expand Down
15 changes: 15 additions & 0 deletions homeassistant/components/tuya/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ class DPCode(StrEnum):
ANION = "anion" # Ionizer unit
ARM_DOWN_PERCENT = "arm_down_percent"
ARM_UP_PERCENT = "arm_up_percent"
ATMOSPHERIC_PRESSTURE = "atmospheric_pressture" # Typo is in Tuya API
BASIC_ANTI_FLICKER = "basic_anti_flicker"
BASIC_DEVICE_VOLUME = "basic_device_volume"
BASIC_FLIP = "basic_flip"
Expand Down Expand Up @@ -215,6 +216,10 @@ class DPCode(StrEnum):
HUMIDITY = "humidity" # Humidity
HUMIDITY_CURRENT = "humidity_current" # Current humidity
HUMIDITY_INDOOR = "humidity_indoor" # Indoor humidity
HUMIDITY_OUTDOOR = "humidity_outdoor" # Outdoor humidity
HUMIDITY_OUTDOOR_1 = "humidity_outdoor_1" # Outdoor humidity
HUMIDITY_OUTDOOR_2 = "humidity_outdoor_2" # Outdoor humidity
HUMIDITY_OUTDOOR_3 = "humidity_outdoor_3" # Outdoor humidity
HUMIDITY_SET = "humidity_set" # Humidity setting
HUMIDITY_VALUE = "humidity_value" # Humidity
IPC_WORK_MODE = "ipc_work_mode"
Expand Down Expand Up @@ -360,6 +365,15 @@ class DPCode(StrEnum):
TEMP_CURRENT_EXTERNAL = (
"temp_current_external" # Current external temperature in Celsius
)
TEMP_CURRENT_EXTERNAL_1 = (
"temp_current_external_1" # Current external temperature in Celsius
)
TEMP_CURRENT_EXTERNAL_2 = (
"temp_current_external_2" # Current external temperature in Celsius
)
TEMP_CURRENT_EXTERNAL_3 = (
"temp_current_external_3" # Current external temperature in Celsius
)
TEMP_CURRENT_EXTERNAL_F = (
"temp_current_external_f" # Current external temperature in Fahrenheit
)
Expand Down Expand Up @@ -405,6 +419,7 @@ class DPCode(StrEnum):
WINDOW_CHECK = "window_check"
WINDOW_STATE = "window_state"
WINDSPEED = "windspeed"
WINDSPEED_AVG = "windspeed_avg"
WIRELESS_BATTERYLOCK = "wireless_batterylock"
WIRELESS_ELECTRICITY = "wireless_electricity"
WORK_MODE = "work_mode" # Working mode
Expand Down
Loading
Loading