Skip to content
Merged
1 change: 1 addition & 0 deletions homeassistant/components/bring/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def __init__(

async def _async_update_data(self) -> dict[str, BringActivityData]:
"""Fetch activity data from bring."""
self.lists = self.coordinator.lists

list_dict: dict[str, BringActivityData] = {}
for lst in self.lists:
Expand Down
5 changes: 3 additions & 2 deletions homeassistant/components/bring/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def add_entities() -> None:
)
lists_added |= new_lists

coordinator.activity.async_add_listener(add_entities)
coordinator.data.async_add_listener(add_entities)
add_entities()


Expand All @@ -67,7 +67,8 @@ def __init__(

def _async_handle_event(self) -> None:
"""Handle the activity event."""
bring_list = self.coordinator.data[self._list_uuid]
if (bring_list := self.coordinator.data.get(self._list_uuid)) is None:
return
last_event_triggered = self.state
if bring_list.activity.timeline and (
last_event_triggered is None
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/compit/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ async def async_setup_entry(

coordinator = entry.runtime_data
climate_entities = []
for device_id in coordinator.connector.devices:
device = coordinator.connector.devices[device_id]
for device_id in coordinator.connector.all_devices:
device = coordinator.connector.all_devices[device_id]

if device.definition.device_class == CLIMATE_DEVICE_CLASS:
climate_entities.append(
Expand Down Expand Up @@ -140,7 +140,8 @@ def __init__(
def available(self) -> bool:
"""Return if entity is available."""
return (
super().available and self.device_id in self.coordinator.connector.devices
super().available
and self.device_id in self.coordinator.connector.all_devices
)

@property
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/compit/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ def __init__(
async def _async_update_data(self) -> dict[int, DeviceInstance]:
"""Update data via library."""
await self.connector.update_state(device_id=None) # Update all devices
return self.connector.devices
return self.connector.all_devices
2 changes: 1 addition & 1 deletion homeassistant/components/compit/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["compit"],
"quality_scale": "bronze",
"requirements": ["compit-inext-api==0.2.1"]
"requirements": ["compit-inext-api==0.3.1"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/ecovacs/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/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.11", "deebot-client==13.7.0"]
"requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"]
}
29 changes: 28 additions & 1 deletion homeassistant/components/ecovacs/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
from collections.abc import Callable
from dataclasses import dataclass

from deebot_client.capabilities import CapabilitySet
from deebot_client.capabilities import CapabilityNumber, CapabilitySet
from deebot_client.device import Device
from deebot_client.events import CleanCountEvent, CutDirectionEvent, VolumeEvent
from deebot_client.events.base import Event
from deebot_client.events.water_info import WaterCustomAmountEvent

from homeassistant.components.number import (
NumberEntity,
Expand Down Expand Up @@ -75,6 +77,19 @@ class EcovacsNumberEntityDescription[EventT: Event](
native_step=1.0,
mode=NumberMode.BOX,
),
EcovacsNumberEntityDescription[WaterCustomAmountEvent](
capability_fn=lambda caps: (
caps.water.amount
if caps.water and isinstance(caps.water.amount, CapabilityNumber)
else None
),
value_fn=lambda e: e.value,
key="water_amount",
translation_key="water_amount",
entity_category=EntityCategory.CONFIG,
native_step=1.0,
mode=NumberMode.BOX,
),
)


Expand All @@ -100,6 +115,18 @@ class EcovacsNumberEntity[EventT: Event](

entity_description: EcovacsNumberEntityDescription

def __init__(
self,
device: Device,
capability: CapabilitySet[EventT, [int]],
entity_description: EcovacsNumberEntityDescription,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description)
if isinstance(capability, CapabilityNumber):
self._attr_native_min_value = capability.min
self._attr_native_max_value = capability.max

async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
Expand Down
6 changes: 5 additions & 1 deletion homeassistant/components/ecovacs/select.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ class EcovacsSelectEntityDescription[EventT: Event](

ENTITY_DESCRIPTIONS: tuple[EcovacsSelectEntityDescription, ...] = (
EcovacsSelectEntityDescription[WaterAmountEvent](
capability_fn=lambda caps: caps.water.amount if caps.water else None,
capability_fn=lambda caps: (
caps.water.amount
if caps.water and isinstance(caps.water.amount, CapabilitySetTypes)
else None
),
current_option_fn=lambda e: get_name_key(e.value),
options_fn=lambda water: [get_name_key(amount) for amount in water.types],
key="water_amount",
Expand Down
9 changes: 7 additions & 2 deletions homeassistant/components/ecovacs/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@
},
"volume": {
"name": "Volume"
},
"water_amount": {
"name": "Water flow level"
}
},
"sensor": {
Expand Down Expand Up @@ -152,8 +155,10 @@
"station_state": {
"name": "Station state",
"state": {
"drying_mop": "Drying mop",
"idle": "[%key:common::state::idle%]",
"emptying_dustbin": "Emptying dustbin"
"emptying_dustbin": "Emptying dustbin",
"washing_mop": "Washing mop"
}
},
"stats_area": {
Expand All @@ -174,7 +179,7 @@
},
"select": {
"water_amount": {
"name": "Water flow level",
"name": "[%key:component::ecovacs::entity::number::water_amount::name%]",
"state": {
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
Expand Down
5 changes: 0 additions & 5 deletions homeassistant/components/ecovacs/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
import string
from typing import TYPE_CHECKING

from deebot_client.events.station import State

from homeassistant.core import HomeAssistant, callback
from homeassistant.util import slugify

Expand Down Expand Up @@ -49,9 +47,6 @@ def get_supported_entities(
@callback
def get_name_key(enum: Enum) -> str:
"""Return the lower case name of the enum."""
if enum is State.EMPTYING:
# Will be fixed in the next major release of deebot-client
return "emptying_dustbin"
return enum.name.lower()


Expand Down
62 changes: 40 additions & 22 deletions homeassistant/components/esphome/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ async def async_step_reauth(
return await self._async_authenticate_or_add()

if error is None and entry_data.get(CONF_NOISE_PSK):
# Device was configured with encryption but now connects without it.
# Check if it's the same device before offering to remove encryption.
if self._reauth_entry.unique_id and self._device_mac:
expected_mac = format_mac(self._reauth_entry.unique_id)
actual_mac = format_mac(self._device_mac)
if expected_mac != actual_mac:
# Different device at the same IP - do not offer to remove encryption
return self._async_abort_wrong_device(
self._reauth_entry, expected_mac, actual_mac
)
return await self.async_step_reauth_encryption_removed_confirm()
return await self.async_step_reauth_confirm()

Expand Down Expand Up @@ -508,6 +518,28 @@ def _async_make_config_data(self) -> dict[str, Any]:
CONF_DEVICE_NAME: self._device_name,
}

@callback
def _async_abort_wrong_device(
self, entry: ConfigEntry, expected_mac: str, actual_mac: str
) -> ConfigFlowResult:
"""Abort flow because a different device was found at the IP address."""
assert self._host is not None
assert self._device_name is not None
if self.source == SOURCE_RECONFIGURE:
reason = "reconfigure_unique_id_changed"
else:
reason = "reauth_unique_id_changed"
return self.async_abort(
reason=reason,
description_placeholders={
"name": entry.data.get(CONF_DEVICE_NAME, entry.title),
"host": self._host,
"expected_mac": expected_mac,
"unexpected_mac": actual_mac,
"unexpected_device_name": self._device_name,
},
)

async def _async_validated_connection(self) -> ConfigFlowResult:
"""Handle validated connection."""
if self.source == SOURCE_RECONFIGURE:
Expand Down Expand Up @@ -539,17 +571,10 @@ async def _async_reauth_validated_connection(self) -> ConfigFlowResult:
# Reauth was triggered a while ago, and since than
# a new device resides at the same IP address.
assert self._device_name is not None
return self.async_abort(
reason="reauth_unique_id_changed",
description_placeholders={
"name": self._reauth_entry.data.get(
CONF_DEVICE_NAME, self._reauth_entry.title
),
"host": self._host,
"expected_mac": format_mac(self._reauth_entry.unique_id),
"unexpected_mac": format_mac(self.unique_id),
"unexpected_device_name": self._device_name,
},
return self._async_abort_wrong_device(
self._reauth_entry,
format_mac(self._reauth_entry.unique_id),
format_mac(self.unique_id),
)

async def _async_reconfig_validated_connection(self) -> ConfigFlowResult:
Expand Down Expand Up @@ -589,17 +614,10 @@ async def _async_reconfig_validated_connection(self) -> ConfigFlowResult:
if self._reconfig_entry.data.get(CONF_DEVICE_NAME) == self._device_name:
self._entry_with_name_conflict = self._reconfig_entry
return await self.async_step_name_conflict()
return self.async_abort(
reason="reconfigure_unique_id_changed",
description_placeholders={
"name": self._reconfig_entry.data.get(
CONF_DEVICE_NAME, self._reconfig_entry.title
),
"host": self._host,
"expected_mac": format_mac(self._reconfig_entry.unique_id),
"unexpected_mac": format_mac(self.unique_id),
"unexpected_device_name": self._device_name,
},
return self._async_abort_wrong_device(
self._reconfig_entry,
format_mac(self._reconfig_entry.unique_id),
format_mac(self.unique_id),
)

async def async_step_encryption_key(
Expand Down
30 changes: 20 additions & 10 deletions homeassistant/components/pooldose/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST
from homeassistant.const import CONF_HOST, CONF_MAC
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo

Expand All @@ -31,9 +31,10 @@ class PooldoseConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1

def __init__(self) -> None:
"""Initialize the config flow and store the discovered IP address."""
"""Initialize the config flow and store the discovered IP address and MAC."""
super().__init__()
self._discovered_ip: str | None = None
self._discovered_mac: str | None = None

async def _validate_host(
self, host: str
Expand Down Expand Up @@ -71,13 +72,20 @@ async def async_step_dhcp(
if not serial_number:
return self.async_abort(reason="no_serial_number")

await self.async_set_unique_id(serial_number)

# Conditionally update IP and abort if entry exists
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})

# Continue with new device flow
# If an existing entry is found
existing_entry = await self.async_set_unique_id(serial_number)
if existing_entry:
# Only update the MAC if it's not already set
if CONF_MAC not in existing_entry.data:
self.hass.config_entries.async_update_entry(
existing_entry,
data={**existing_entry.data, CONF_MAC: discovery_info.macaddress},
)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})

# Else: Continue with new flow
self._discovered_ip = discovery_info.ip
self._discovered_mac = discovery_info.macaddress
return self.async_show_form(
step_id="dhcp_confirm",
description_placeholders={
Expand All @@ -91,10 +99,12 @@ async def async_step_dhcp_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Create the entry after the confirmation dialog."""
discovered_ip = self._discovered_ip
return self.async_create_entry(
title=f"PoolDose {self.unique_id}",
data={CONF_HOST: discovered_ip},
data={
CONF_HOST: self._discovered_ip,
CONF_MAC: self._discovered_mac,
},
)

async def async_step_user(
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/pooldose/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class PooldoseCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for PoolDose integration."""

device_info: dict[str, Any]
config_entry: PooldoseConfigEntry

def __init__(
self,
Expand Down
14 changes: 11 additions & 3 deletions homeassistant/components/pooldose/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@

from typing import Any

from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.const import CONF_MAC
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN, MANUFACTURER
from .coordinator import PooldoseCoordinator


def device_info(info: dict | None, unique_id: str) -> DeviceInfo:
def device_info(
info: dict | None, unique_id: str, mac: str | None = None
) -> DeviceInfo:
"""Create device info for PoolDose devices."""
if info is None:
info = {}
Expand All @@ -35,6 +38,7 @@ def device_info(info: dict | None, unique_id: str) -> DeviceInfo:
configuration_url=(
f"http://{info['IP']}/index.html" if info.get("IP") else None
),
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
)


Expand All @@ -56,7 +60,11 @@ def __init__(
self.entity_description = entity_description
self.platform_name = platform_name
self._attr_unique_id = f"{serial_number}_{entity_description.key}"
self._attr_device_info = device_info(device_properties, serial_number)
self._attr_device_info = device_info(
device_properties,
serial_number,
coordinator.config_entry.data.get(CONF_MAC),
)

@property
def available(self) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/sonos/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["soco", "sonos_websocket"],
"quality_scale": "bronze",
"requirements": ["soco==0.30.11", "sonos-websocket==0.1.3"],
"requirements": ["soco==0.30.12", "sonos-websocket==0.1.3"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:ZonePlayer:1"
Expand Down
Loading
Loading