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
7 changes: 6 additions & 1 deletion homeassistant/components/airobot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@

from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator

PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.NUMBER, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]


async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
Expand Down
89 changes: 89 additions & 0 deletions homeassistant/components/airobot/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Button platform for Airobot integration."""

from __future__ import annotations

from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any

from pyairobotrest.exceptions import (
AirobotConnectionError,
AirobotError,
AirobotTimeoutError,
)

from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .const import DOMAIN
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
from .entity import AirobotEntity

PARALLEL_UPDATES = 0


@dataclass(frozen=True, kw_only=True)
class AirobotButtonEntityDescription(ButtonEntityDescription):
"""Describes Airobot button entity."""

press_fn: Callable[[AirobotDataUpdateCoordinator], Coroutine[Any, Any, None]]


BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
AirobotButtonEntityDescription(
key="restart",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
),
)


async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot button entities."""
coordinator = entry.runtime_data

async_add_entities(
AirobotButton(coordinator, description) for description in BUTTON_TYPES
)


class AirobotButton(AirobotEntity, ButtonEntity):
"""Representation of an Airobot button."""

entity_description: AirobotButtonEntityDescription

def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
description: AirobotButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"

async def async_press(self) -> None:
"""Handle the button press."""
try:
await self.entity_description.press_fn(self.coordinator)
except (AirobotConnectionError, AirobotTimeoutError):
# Connection errors during reboot are expected as device restarts
pass
except AirobotError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="button_press_failed",
translation_placeholders={"button": self.entity_description.key},
) from err
3 changes: 3 additions & 0 deletions homeassistant/components/airobot/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."
},
"button_press_failed": {
"message": "Failed to press {button} button."
},
"connection_failed": {
"message": "Failed to communicate with device."
},
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/duckdns/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Integrate with DuckDNS."""
"""Duck DNS integration."""

from __future__ import annotations

Expand Down
31 changes: 24 additions & 7 deletions homeassistant/components/duckdns/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

from __future__ import annotations

from aiohttp import ClientError
import voluptuous as vol

from homeassistant.const import CONF_ACCESS_TOKEN, CONF_DOMAIN
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import ConfigEntrySelector
Expand Down Expand Up @@ -62,9 +63,25 @@ async def update_domain_service(call: ServiceCall) -> None:

session = async_get_clientsession(call.hass)

await update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
txt=call.data.get(ATTR_TXT),
)
try:
if not await update_duckdns(
session,
entry.data[CONF_DOMAIN],
entry.data[CONF_ACCESS_TOKEN],
txt=call.data.get(ATTR_TXT),
):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={
CONF_DOMAIN: entry.data[CONF_DOMAIN],
},
)
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={
CONF_DOMAIN: entry.data[CONF_DOMAIN],
},
) from e
2 changes: 1 addition & 1 deletion homeassistant/components/opower/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.15.9"]
"requirements": ["opower==0.16.0"]
}
2 changes: 1 addition & 1 deletion homeassistant/components/system_bridge/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["systembridgeconnector"],
"requirements": ["systembridgeconnector==5.2.4"],
"requirements": ["systembridgeconnector==5.3.1"],
"zeroconf": ["_system-bridge._tcp.local."]
}
1 change: 1 addition & 0 deletions homeassistant/components/tuya/diagnostics.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ def _async_device_as_dict(
data["status_range"][status_range.code] = {
"type": status_range.type,
"value": status_range.values,
"report_type": status_range.report_type,
}

# Gather information how this Tuya device is represented in Home Assistant
Expand Down
16 changes: 16 additions & 0 deletions homeassistant/components/xbox/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntryType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import DOMAIN
Expand Down Expand Up @@ -115,6 +117,20 @@ async def update_data(self) -> dict[str, SmartglassConsole]:
"Found %d consoles: %s", len(consoles.result), consoles.model_dump()
)

device_reg = dr.async_get(self.hass)
identifiers = {(DOMAIN, console.id) for console in consoles.result}
for device in dr.async_entries_for_config_entry(
device_reg, self.config_entry.entry_id
):
if (
device.entry_type is not DeviceEntryType.SERVICE
and not set(device.identifiers) & identifiers
):
_LOGGER.debug("Removing stale device %s", device.name)
device_reg.async_update_device(
device.id, remove_config_entry_id=self.config_entry.entry_id
)

return {console.id: console for console in consoles.result}


Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/xbox/entity.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,11 @@ def data(self) -> ConsoleData:
"""Return coordinator data for this console."""
return self.coordinator.data[self._console.id]

@property
def available(self) -> bool:
"""Return if entity is available."""
return self.coordinator.data.get(self._console.id) is not None


def check_deprecated_entity(
hass: HomeAssistant,
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/xbox/media_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ def add_entities() -> None:
]
)
devices_added |= new_devices
devices_added &= set(consoles.data)

entry.async_on_unload(consoles.async_add_listener(add_entities))
add_entities()
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/xbox/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def add_entities() -> None:
)

devices_added |= new_devices
devices_added &= set(consoles.data)

entry.async_on_unload(consoles.async_add_listener(add_entities))
add_entities()
Expand Down
1 change: 1 addition & 0 deletions homeassistant/components/xbox/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ def add_entities() -> None:
]
)
devices_added |= new_devices
devices_added &= set(consoles.data)

config_entry.async_on_unload(consoles.async_add_listener(add_entities))
add_entities()
Expand Down
4 changes: 2 additions & 2 deletions requirements_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions requirements_test_all.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

50 changes: 50 additions & 0 deletions tests/components/airobot/snapshots/test_button.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# serializer version: 1
# name: test_buttons[button.test_thermostat_restart-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'button',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'button.test_thermostat_restart',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': <ButtonDeviceClass.RESTART: 'restart'>,
'original_icon': None,
'original_name': 'Restart',
'platform': 'airobot',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': None,
'unique_id': 'T01A1B2C3_restart',
'unit_of_measurement': None,
})
# ---
# name: test_buttons[button.test_thermostat_restart-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'restart',
'friendly_name': 'Test Thermostat Restart',
}),
'context': <ANY>,
'entity_id': 'button.test_thermostat_restart',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'unknown',
})
# ---
Loading
Loading