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
33 changes: 33 additions & 0 deletions homeassistant/components/airos/diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Diagnostics support for airOS."""

from __future__ import annotations

from typing import Any

from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_PASSWORD
from homeassistant.core import HomeAssistant

from .coordinator import AirOSConfigEntry

IP_REDACT = ["addr", "ipaddr", "ip6addr", "lastip"] # IP related
HW_REDACT = ["apmac", "hwaddr", "mac"] # MAC address
TO_REDACT_HA = [CONF_HOST, CONF_PASSWORD]
TO_REDACT_AIROS = [
"hostname", # Prevent leaking device naming
"essid", # Network SSID
"lat", # GPS latitude to prevent exposing location data.
"lon", # GPS longitude to prevent exposing location data.
*HW_REDACT,
*IP_REDACT,
]


async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirOSConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_HA),
"data": async_redact_data(entry.runtime_data.data.to_dict(), TO_REDACT_AIROS),
}
2 changes: 1 addition & 1 deletion homeassistant/components/airos/quality_scale.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ rules:

# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info: todo
discovery: todo
docs-data-update: done
Expand Down
1 change: 0 additions & 1 deletion homeassistant/components/analytics/analytics.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,6 @@ async def async_devices_payload(hass: HomeAssistant) -> dict:
"model": device.model,
"sw_version": device.sw_version,
"hw_version": device.hw_version,
"has_suggested_area": device.suggested_area is not None,
"has_configuration_url": device.configuration_url is not None,
"via_device": None,
}
Expand Down
3 changes: 3 additions & 0 deletions homeassistant/components/enphase_envoy/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ async def async_get_config_entry_diagnostics(
entities.append({"entity": entity_dict, "state": state_dict})
device_dict = asdict(device)
device_dict.pop("_cache", None)
# This can be removed when suggested_area is removed from DeviceEntry
device_dict.pop("_suggested_area")
device_dict.pop("is_new", None)
device_entities.append({"device": device_dict, "entities": entities})

# remove envoy serial
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/hassio/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"healthy": "Healthy",
"host_os": "Host operating system",
"installed_addons": "Installed add-ons",
"nameservers": "Nameservers",
"supervisor_api": "Supervisor API",
"supervisor_version": "Supervisor version",
"supported": "Supported",
Expand Down
10 changes: 10 additions & 0 deletions homeassistant/components/hassio/system_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"error": "Unsupported",
}

nameservers = set()
for interface in network_info.get("interfaces", []):
if not interface.get("primary"):
continue
if ipv4 := interface.get("ipv4"):
nameservers.update(ipv4.get("nameservers", []))
if ipv6 := interface.get("ipv6"):
nameservers.update(ipv6.get("nameservers", []))

information = {
"host_os": host_info.get("operating_system"),
"update_channel": info.get("channel"),
Expand All @@ -62,6 +71,7 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"docker_version": info.get("docker"),
"disk_total": f"{host_info.get('disk_total')} GB",
"disk_used": f"{host_info.get('disk_used')} GB",
"nameservers": ", ".join(nameservers),
"healthy": healthy,
"supported": supported,
"host_connectivity": network_info.get("host_internet"),
Expand Down
14 changes: 14 additions & 0 deletions homeassistant/components/motion_blinds/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,17 +289,23 @@ async def async_open_cover_tilt(self, **kwargs: Any) -> None:
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 180)

await self.async_request_position_till_stop()

async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, 0)

await self.async_request_position_till_stop()

async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_angle, angle)

await self.async_request_position_till_stop()

async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
"""Stop the cover."""
async with self._api_lock:
Expand Down Expand Up @@ -360,11 +366,15 @@ async def async_open_cover_tilt(self, **kwargs: Any) -> None:
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Open)

await self.async_request_position_till_stop()

async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Close the cover tilt."""
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Close)

await self.async_request_position_till_stop()

async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Move the cover tilt to a specific position."""
angle = kwargs[ATTR_TILT_POSITION]
Expand All @@ -376,6 +386,8 @@ async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_position, angle)

await self.async_request_position_till_stop()

async def async_set_absolute_position(self, **kwargs):
"""Move the cover to a specific absolute position (see TDBU)."""
angle = kwargs.get(ATTR_TILT_POSITION)
Expand All @@ -390,6 +402,8 @@ async def async_set_absolute_position(self, **kwargs):
async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Set_position, angle)

await self.async_request_position_till_stop()


class MotionTDBUDevice(MotionBaseDevice):
"""Representation of a Motion Top Down Bottom Up blind Device."""
Expand Down
21 changes: 17 additions & 4 deletions homeassistant/components/motion_blinds/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ def __init__(

self._requesting_position: CALLBACK_TYPE | None = None
self._previous_positions: list[int | dict | None] = []
self._previous_angles: list[int | None] = []

if blind.device_type in DEVICE_TYPES_WIFI:
self._update_interval_moving = UPDATE_INTERVAL_MOVING_WIFI
Expand Down Expand Up @@ -112,17 +113,27 @@ async def async_scheduled_update_request(self, *_) -> None:
"""Request a state update from the blind at a scheduled point in time."""
# add the last position to the list and keep the list at max 2 items
self._previous_positions.append(self._blind.position)
self._previous_angles.append(self._blind.angle)
if len(self._previous_positions) > 2:
del self._previous_positions[: len(self._previous_positions) - 2]
if len(self._previous_angles) > 2:
del self._previous_angles[: len(self._previous_angles) - 2]

async with self._api_lock:
await self.hass.async_add_executor_job(self._blind.Update_trigger)

self.coordinator.async_update_listeners()

if len(self._previous_positions) < 2 or not all(
self._blind.position == prev_position
for prev_position in self._previous_positions
if (
len(self._previous_positions) < 2
or not all(
self._blind.position == prev_position
for prev_position in self._previous_positions
)
or len(self._previous_angles) < 2
or not all(
self._blind.angle == prev_angle for prev_angle in self._previous_angles
)
):
# keep updating the position @self._update_interval_moving until the position does not change.
self._requesting_position = async_call_later(
Expand All @@ -132,6 +143,7 @@ async def async_scheduled_update_request(self, *_) -> None:
)
else:
self._previous_positions = []
self._previous_angles = []
self._requesting_position = None

async def async_request_position_till_stop(self, delay: int | None = None) -> None:
Expand All @@ -140,7 +152,8 @@ async def async_request_position_till_stop(self, delay: int | None = None) -> No
delay = self._update_interval_moving

self._previous_positions = []
if self._blind.position is None:
self._previous_angles = []
if self._blind.position is None and self._blind.angle is None:
return
if self._requesting_position is not None:
self._requesting_position()
Expand Down
34 changes: 21 additions & 13 deletions homeassistant/components/tuya/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
ColorMode,
LightEntity,
LightEntityDescription,
color_supported,
filter_supported_color_modes,
)
from homeassistant.const import EntityCategory
Expand Down Expand Up @@ -530,19 +531,6 @@ def __init__(
description.brightness_min, dptype=DPType.INTEGER
)

if int_type := self.find_dpcode(
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
):
self._color_temp = int_type
color_modes.add(ColorMode.COLOR_TEMP)
# If entity does not have color_temp, check if it has work_mode "white"
elif color_mode_enum := self.find_dpcode(
description.color_mode, dptype=DPType.ENUM, prefer_function=True
):
if WorkMode.WHITE.value in color_mode_enum.range:
color_modes.add(ColorMode.WHITE)
self._white_color_mode = ColorMode.WHITE

if (
dpcode := self.find_dpcode(description.color_data, prefer_function=True)
) and self.get_dptype(dpcode) == DPType.JSON:
Expand All @@ -568,6 +556,26 @@ def __init__(
):
self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2

# Check if the light has color temperature
if int_type := self.find_dpcode(
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
):
self._color_temp = int_type
color_modes.add(ColorMode.COLOR_TEMP)
# If light has color but does not have color_temp, check if it has
# work_mode "white"
elif (
color_supported(color_modes)
and (
color_mode_enum := self.find_dpcode(
description.color_mode, dptype=DPType.ENUM, prefer_function=True
)
)
and WorkMode.WHITE.value in color_mode_enum.range
):
color_modes.add(ColorMode.WHITE)
self._white_color_mode = ColorMode.WHITE

self._attr_supported_color_modes = filter_supported_color_modes(color_modes)
if len(self._attr_supported_color_modes) == 1:
# If the light supports only a single color mode, set it now
Expand Down
16 changes: 15 additions & 1 deletion homeassistant/helpers/device_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

from . import storage, translation
from .debounce import Debouncer
from .deprecation import deprecated_function
from .frame import ReportBehavior, report_usage
from .json import JSON_DUMP, find_paths_unserializable_data, json_bytes, json_fragment
from .registry import BaseRegistry, BaseRegistryItems, RegistryIndexType
Expand Down Expand Up @@ -67,6 +68,7 @@

ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30

# Can be removed when suggested_area is removed from DeviceEntry
RUNTIME_ONLY_ATTRS = {"suggested_area"}

CONFIGURATION_URL_SCHEMES = {"http", "https", "homeassistant"}
Expand Down Expand Up @@ -343,7 +345,8 @@ class DeviceEntry:
name: str | None = attr.ib(default=None)
primary_config_entry: str | None = attr.ib(default=None)
serial_number: str | None = attr.ib(default=None)
suggested_area: str | None = attr.ib(default=None)
# Suggested area is deprecated and will be removed from DeviceEntry in 2026.9.
_suggested_area: str | None = attr.ib(default=None)
sw_version: str | None = attr.ib(default=None)
via_device_id: str | None = attr.ib(default=None)
# This value is not stored, just used to keep track of events to fire.
Expand Down Expand Up @@ -442,6 +445,14 @@ def as_storage_fragment(self) -> json_fragment:
)
)

@property
@deprecated_function(
"code which ignores suggested_area", breaks_in_ha_version="2026.9"
)
def suggested_area(self) -> str | None:
"""Return the suggested area for this device entry."""
return self._suggested_area


@attr.s(frozen=True, slots=True)
class DeletedDeviceEntry:
Expand Down Expand Up @@ -1197,6 +1208,7 @@ def async_update_device( # noqa: C901
("name", name),
("name_by_user", name_by_user),
("serial_number", serial_number),
# Can be removed when suggested_area is removed from DeviceEntry
("suggested_area", suggested_area),
("sw_version", sw_version),
("via_device_id", via_device_id),
Expand All @@ -1211,6 +1223,7 @@ def async_update_device( # noqa: C901
if not new_values:
return old

# This condition can be removed when suggested_area is removed from DeviceEntry
if not RUNTIME_ONLY_ATTRS.issuperset(new_values):
# Change modified_at if we are changing something that we store
new_values["modified_at"] = utcnow()
Expand All @@ -1233,6 +1246,7 @@ def async_update_device( # noqa: C901
# firing events for data we have nothing to compare
# against since its never saved on disk
if RUNTIME_ONLY_ATTRS.issuperset(new_values):
# This can be removed when suggested_area is removed from DeviceEntry
return new

self.async_schedule_save()
Expand Down
2 changes: 0 additions & 2 deletions tests/components/acaia/snapshots/test_init.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
'aa:bb:cc:dd:ee:ff',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'Acaia',
Expand All @@ -31,7 +30,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': None,
'suggested_area': 'Kitchen',
'sw_version': None,
'via_device_id': None,
})
Expand Down
4 changes: 0 additions & 4 deletions tests/components/airgradient/snapshots/test_init.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
'84fce612f5b8',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'AirGradient',
Expand All @@ -31,7 +30,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '84fce612f5b8',
'suggested_area': None,
'sw_version': '3.1.1',
'via_device_id': None,
})
Expand All @@ -58,7 +56,6 @@
'84fce612f5b8',
),
}),
'is_new': False,
'labels': set({
}),
'manufacturer': 'AirGradient',
Expand All @@ -68,7 +65,6 @@
'name_by_user': None,
'primary_config_entry': <ANY>,
'serial_number': '84fce612f5b8',
'suggested_area': None,
'sw_version': '3.1.1',
'via_device_id': None,
})
Expand Down
Loading
Loading