Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
875e062
Update honeywell device cleanup test (#178441)
emontnemery Aug 7, 2026
c67174a
Bump pytrydan to 1.0.5 (#178447)
dgomes Aug 7, 2026
1e2209c
Use product name as model and add hardware version for BleBox devices…
bkobus-bbx Aug 7, 2026
44f553e
Use SmartThings cooling setpoint range for air conditioners (#178430)
StellarSea Aug 7, 2026
84a5b65
Bump solaredge-web to 0.3.1 (#178437)
tronikos Aug 7, 2026
34d72cf
mikrotik: Don't reuse a string from Vodafone Station (#178455)
reedy Aug 7, 2026
49a4101
Migrate calls to async_get_device in ruckus_unleashed tests (#178417)
emontnemery Aug 7, 2026
61d0450
Migrate calls to async_get_device in duco tests (#178367)
emontnemery Aug 7, 2026
1691d54
Migrate calls to async_get_device in tests (part 7) (#178365)
emontnemery Aug 7, 2026
fb4b249
Migrate calls to async_get_device in homekit_controller tests (#178356)
emontnemery Aug 7, 2026
24ef58d
Migrate calls to async_get_device in zwave_js tests (#178355)
emontnemery Aug 7, 2026
0b9a779
Migrate calls to async_get_device in tasmota tests (#178343)
emontnemery Aug 7, 2026
43618ef
Migrate calls to async_get_device in tests (part 6) (#178339)
emontnemery Aug 7, 2026
c06bab0
Bump lyngdorf to 1.4.4 (#178401)
fishloa Aug 7, 2026
ab3451b
chore: update python-picnic-api2 to v2.0.1 (#178395)
codesalatdev Aug 7, 2026
489ba28
Switchbot Cloud:Enable webhook for the Light series (#178422)
XiaoLing-git Aug 7, 2026
6c1b0c0
Switchbot Cloud: Add new supported devices[Permanent Outdoor Lights] …
XiaoLing-git Aug 7, 2026
23c0ac9
Track shielded service call task in REST API (#178377)
arturpragacz Aug 7, 2026
aed6e95
Migrate calls to async_get_device in shelly tests (#178418)
emontnemery Aug 7, 2026
7843ebb
Migrate calls to async_get_device in unifi_access tests (#178419)
emontnemery Aug 7, 2026
e73975f
Call async_remove_device to remove device in entity registry tests (#…
emontnemery Aug 7, 2026
ea676a9
Adapt LLM integration migration tests to single config entry devices …
emontnemery Aug 7, 2026
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
18 changes: 11 additions & 7 deletions homeassistant/components/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,18 @@ def _async_save_changed_entities(

try:
# shield the service call from cancellation on connection drop
# and track the task so it cannot be garbage collected mid-run
response = await shield(
hass.services.async_call(
domain,
service,
data, # type: ignore[arg-type]
blocking=True,
context=context,
return_response=response_requested,
hass.async_create_task(
hass.services.async_call(
domain,
service,
data, # type: ignore[arg-type]
blocking=True,
context=context,
return_response=response_requested,
),
f"api service call {domain}.{service}",
)
)
except (vol.Invalid, ServiceNotFound) as ex:
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/blebox/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def async_get_config_entry_diagnostics(
"name": product.name,
"type": product.type,
"model": product.model,
"product": product.product,
"unique_id": product.unique_id,
"firmware_version": product.firmware_version,
"hardware_version": product.hardware_version,
Expand Down
3 changes: 2 additions & 1 deletion homeassistant/components/blebox/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ def __init__(self, coordinator: BleBoxCoordinator, feature: _FeatureT) -> None:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, product.unique_id)},
manufacturer=product.brand,
model=product.model,
model=product.product,
name=product.name,
sw_version=product.firmware_version,
hw_version=product.hardware_version,
configuration_url=f"http://{product.address}",
)
2 changes: 1 addition & 1 deletion homeassistant/components/lyngdorf/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["lyngdorf", "async_upnp_client"],
"quality_scale": "silver",
"requirements": ["lyngdorf==1.4.3"],
"requirements": ["lyngdorf==1.4.4"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2",
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/mikrotik/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::vodafone_station::config::step::user::data_description::password%]"
"password": "The password for your Mikrotik device."
},
"description": "The password for {username} is invalid.",
"title": "[%key:common::config_flow::title::reauth%]"
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/picnic/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ async def _async_finish(
CONF_ACCESS_TOKEN: auth_token,
CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE],
}
existing_entry = await self.async_set_unique_id(user_data["user_id"])
existing_entry = await self.async_set_unique_id(user_data.user_id)

# Abort if we're adding a new config and the unique id
# is already in use, else create the entry
Expand Down
134 changes: 76 additions & 58 deletions homeassistant/components/picnic/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

import asyncio
from contextlib import suppress
import copy
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override

from python_picnic_api2 import PicnicAPI
from python_picnic_api2.models import Cart, DeliverySummary, Slot
from python_picnic_api2.session import PicnicAuthError

from homeassistant.config_entries import ConfigEntry
Expand All @@ -32,6 +33,25 @@
type PicnicConfigEntry = ConfigEntry[PicnicUpdateCoordinator]


@dataclass
class NextDeliveryData:
"""The next (current, undelivered) delivery, with its live ETA."""

delivery: DeliverySummary | None = None
eta_start: str | None = None
eta_end: str | None = None
estimated_arrival: int | None = None


@dataclass
class LastOrderData:
"""The most recent delivery, with its total price."""

delivery: DeliverySummary | None = None
total_price: int = 0
delivery_time_start: str | None = None


class PicnicUpdateCoordinator(DataUpdateCoordinator):
"""The coordinator to fetch data from the Picnic API at a set interval."""

Expand Down Expand Up @@ -86,21 +106,20 @@ async def _async_update_data(self) -> dict:
return data

@staticmethod
def _get_update_interval(next_delivery: dict | None) -> timedelta:
def _get_update_interval(next_delivery: NextDeliveryData | None) -> timedelta:
"""Poll faster around the delivery so the live ETA is picked up in time."""
if not next_delivery:
if next_delivery is None or next_delivery.delivery is None:
return DEFAULT_UPDATE_INTERVAL

eta = next_delivery.get("eta")
slot = next_delivery.get("slot")
slot = next_delivery.delivery.slot

start = end = None
if eta:
start = dt_util.parse_datetime(str(eta.get("start")))
end = dt_util.parse_datetime(str(eta.get("end")))
if next_delivery.eta_start and next_delivery.eta_end:
start = dt_util.parse_datetime(next_delivery.eta_start)
end = dt_util.parse_datetime(next_delivery.eta_end)
if (start is None or end is None) and slot:
start = dt_util.parse_datetime(str(slot.get("window_start")))
end = dt_util.parse_datetime(str(slot.get("window_end")))
start = dt_util.parse_datetime(str(slot.window_start))
end = dt_util.parse_datetime(str(slot.window_end))

if start is None or end is None:
return DEFAULT_UPDATE_INTERVAL
Expand Down Expand Up @@ -129,98 +148,97 @@ def fetch_data(self):
raise UpdateFailed("API response doesn't contain expected data.")

next_delivery, last_order = self._get_order_data()
slot_data = self._get_slot_data(cart)

return {
ADDRESS: self._get_address(),
CART_DATA: cart,
SLOT_DATA: slot_data,
SLOT_DATA: self._get_slot_data(cart),
NEXT_DELIVERY_DATA: next_delivery,
LAST_ORDER_DATA: last_order,
}

def _get_address(self):
"""Get the address that identifies the Picnic service."""
if self._user_address is None:
address = self.picnic_api_client.get_user()["address"]
address = self.picnic_api_client.get_user().address
self._user_address = (
f"{address['street']} "
f"{address['house_number']}{address['house_number_ext']}"
f"{address.street} "
f"{address.house_number}{address.house_number_ext or ''}"
)

return self._user_address

@staticmethod
def _get_slot_data(cart: dict) -> dict:
def _get_slot_data(cart: Cart) -> Slot | None:
"""Get the selected slot, if it's explicitly selected."""
selected_slot = cart.get("selected_slot", {})
available_slots = cart.get("delivery_slots", [])
selected_slot = cart.selected_slot

if selected_slot.get("state") == "EXPLICIT":
slot_data = filter(
lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"),
available_slots,
)
if slot_data:
return next(slot_data)
if selected_slot and selected_slot.state == "EXPLICIT":
for slot in cart.delivery_slots:
if slot.slot_id == selected_slot.slot_id:
return slot

return {}
return None

def _get_order_data(self) -> tuple[dict, dict]:
@staticmethod
def _delivery_time(delivery: DeliverySummary) -> dict | None:
"""Return the raw delivery-time window; not a field the library models."""
return delivery.raw.get("delivery_time") if delivery.raw else None

def _get_order_data(self) -> tuple[NextDeliveryData, LastOrderData]:
"""Get data of the last order from the list of deliveries."""
# Get the deliveries
deliveries = self.picnic_api_client.get_deliveries(summary=True)

# Determine the last order and return an empty dict if there is none
# Determine the last order and return empty data if there is none
try:
# Filter on status CURRENT and select the last
# on the list which is the first one to be delivered
# Make a deepcopy because some references are local
next_deliveries = list(
filter(lambda d: d["status"] == "CURRENT", deliveries)
)
next_delivery = (
copy.deepcopy(next_deliveries[-1]) if next_deliveries else {}
)
last_order = copy.deepcopy(deliveries[0]) if deliveries else {}
except KeyError, TypeError:
# A KeyError or TypeError indicate that the
next_deliveries = [d for d in deliveries if d.status == "CURRENT"]
next_delivery = next_deliveries[-1] if next_deliveries else None
last_order = deliveries[0] if deliveries else None
except AttributeError, TypeError:
# An AttributeError or TypeError indicate that the
# response contains unexpected data
return {}, {}
return NextDeliveryData(), LastOrderData()

if last_order is None:
return NextDeliveryData(), LastOrderData()

# Get the next order's position details if there is an undelivered order
delivery_position = {}
if next_delivery and not next_delivery.get("delivery_time"):
if next_delivery and not self._delivery_time(next_delivery):
# ValueError: If no information yet can mean an empty response
with suppress(ValueError):
delivery_position = self.picnic_api_client.get_delivery_position(
next_delivery["delivery_id"]
next_delivery.delivery_id
)

# Determine the ETA, if available, the one from the
# delivery position API is more precise
# but, it's only available shortly before the actual delivery.
next_delivery["eta"] = delivery_position.get(
"eta_window", next_delivery.get("eta2", {})
eta_window = delivery_position.get("eta_window") or {}
eta2 = next_delivery.eta2 if next_delivery else None
next_delivery_data = NextDeliveryData(
delivery=next_delivery,
eta_start=eta_window.get("start") or (eta2.start if eta2 else None),
eta_end=eta_window.get("end") or (eta2.end if eta2 else None),
# The position response's eta (unix timestamp in milliseconds) feeds
# the estimated arrival sensor; the API only serves it shortly before
# the delivery, so that sensor is unknown outside that window
estimated_arrival=delivery_position.get("eta"),
)
if "eta2" in next_delivery:
del next_delivery["eta2"]

# The position response's eta (unix timestamp in milliseconds) feeds
# the estimated arrival sensor; the API only serves it shortly before
# the delivery, so that sensor is unknown outside that window
next_delivery["estimated_arrival"] = delivery_position.get("eta")

# Determine the total price by adding up the total price of all sub-orders
total_price = 0
for order in last_order.get("orders", []):
total_price += order.get("total_price", 0)
last_order["total_price"] = total_price

# Make sure delivery_time is a dict
last_order.setdefault("delivery_time", {})
total_price = sum(order.total_price or 0 for order in last_order.orders)
delivery_time = self._delivery_time(last_order)
last_order_data = LastOrderData(
delivery=last_order,
total_price=total_price,
delivery_time_start=delivery_time.get("start") if delivery_time else None,
)

return next_delivery, last_order
return next_delivery_data, last_order_data

@callback
def _update_auth_token(self):
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/picnic/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_picnic_api2"],
"requirements": ["python-picnic-api2==1.3.4"]
"requirements": ["python-picnic-api2==2.0.1"]
}
Loading
Loading