forked from home-assistant/core
Compare commits
3 Commits
2025.6.0
...
mqtt-suben
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6da2ea28fc | ||
|
|
e7b5c599dc | ||
|
|
ec41abd821 |
@@ -65,7 +65,6 @@ homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
|
||||
5
CODEOWNERS
generated
5
CODEOWNERS
generated
@@ -89,8 +89,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
@@ -305,7 +303,6 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/crownstone/ @Crownstone @RicArch97
|
||||
/tests/components/crownstone/ @Crownstone @RicArch97
|
||||
/homeassistant/components/cups/ @fabaff
|
||||
/tests/components/cups/ @fabaff
|
||||
/homeassistant/components/daikin/ @fredrike
|
||||
/tests/components/daikin/ @fredrike
|
||||
/homeassistant/components/date/ @home-assistant/core
|
||||
@@ -1420,8 +1417,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/sma/ @kellerza @rklomp @erwindouna
|
||||
/homeassistant/components/smappee/ @bsmappee
|
||||
/tests/components/smappee/ @bsmappee
|
||||
/homeassistant/components/smarla/ @explicatis @rlint-explicatis
|
||||
/tests/components/smarla/ @explicatis @rlint-explicatis
|
||||
/homeassistant/components/smart_meter_texas/ @grahamwetzler
|
||||
/tests/components/smart_meter_texas/ @grahamwetzler
|
||||
/homeassistant/components/smartthings/ @joostlek
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "Amazon",
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"alexa_devices",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"domain": "shelly",
|
||||
"name": "shelly",
|
||||
"integrations": ["shelly"],
|
||||
"iot_standards": ["zwave"]
|
||||
}
|
||||
@@ -40,10 +40,9 @@ class AcmedaFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
entry.unique_id for entry in self._async_current_entries()
|
||||
}
|
||||
|
||||
hubs: list[aiopulse.Hub] = []
|
||||
with suppress(TimeoutError):
|
||||
async with timeout(5):
|
||||
hubs = [
|
||||
hubs: list[aiopulse.Hub] = [
|
||||
hub
|
||||
async for hub in aiopulse.Hub.discover()
|
||||
if hub.id not in already_configured
|
||||
|
||||
@@ -51,16 +51,9 @@ class AirGradientCoordinator(DataUpdateCoordinator[AirGradientData]):
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
try:
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
except AirGradientError as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
self._current_version = (
|
||||
await self.client.get_current_measures()
|
||||
).firmware_version
|
||||
|
||||
async def _async_update_data(self) -> AirGradientData:
|
||||
try:
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""Alexa Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NOTIFY,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Alexa Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
await entry.runtime_data.api.close()
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Support for binary sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: Final = (
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="online",
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
is_on_fn=lambda _device: _device.online,
|
||||
),
|
||||
AmazonBinarySensorEntityDescription(
|
||||
key="bluetooth",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
translation_key="bluetooth",
|
||||
is_on_fn=lambda _device: _device.bluetooth_state,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices binary sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonBinarySensorEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in BINARY_SENSORS
|
||||
for serial_num in coordinator.data
|
||||
)
|
||||
|
||||
|
||||
class AmazonBinarySensorEntity(AmazonEntity, BinarySensorEntity):
|
||||
"""Binary sensor device."""
|
||||
|
||||
entity_description: AmazonBinarySensorEntityDescription
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
@@ -1,63 +0,0 @@
|
||||
"""Config flow for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonEchoApi
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.selector import CountrySelector
|
||||
|
||||
from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors = {}
|
||||
if user_input:
|
||||
client = AmazonEchoApi(
|
||||
user_input[CONF_COUNTRY],
|
||||
user_input[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
data = await client.login_mode_interactive(user_input[CONF_CODE])
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except CannotAuthenticate:
|
||||
errors["base"] = "invalid_auth"
|
||||
else:
|
||||
await self.async_set_unique_id(data["customer_info"]["user_id"])
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input.pop(CONF_CODE)
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data=user_input | {CONF_LOGIN_DATA: data},
|
||||
)
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_COUNTRY, default=self.hass.config.country
|
||||
): CountrySelector(),
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Required(CONF_CODE): cv.string,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
"""Alexa Devices constants."""
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
@@ -1,58 +0,0 @@
|
||||
"""Support for Alexa Devices."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||
from aioamazondevices.exceptions import (
|
||||
CannotAuthenticate,
|
||||
CannotConnect,
|
||||
CannotRetrieveData,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import _LOGGER, CONF_LOGIN_DATA
|
||||
|
||||
SCAN_INTERVAL = 30
|
||||
|
||||
type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Base coordinator for Alexa Devices."""
|
||||
|
||||
config_entry: AmazonConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=entry.title,
|
||||
config_entry=entry,
|
||||
update_interval=timedelta(seconds=SCAN_INTERVAL),
|
||||
)
|
||||
self.api = AmazonEchoApi(
|
||||
entry.data[CONF_COUNTRY],
|
||||
entry.data[CONF_USERNAME],
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
try:
|
||||
await self.api.login_mode_stored_data()
|
||||
return await self.api.get_devices_data()
|
||||
except (CannotConnect, CannotRetrieveData) as err:
|
||||
raise UpdateFailed(f"Error occurred while updating {self.name}") from err
|
||||
except CannotAuthenticate as err:
|
||||
raise ConfigEntryError("Could not authenticate") from err
|
||||
@@ -1,66 +0,0 @@
|
||||
"""Diagnostics support for Alexa Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME, CONF_NAME, "title"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
devices: list[dict[str, dict[str, Any]]] = [
|
||||
build_device_data(device) for device in coordinator.data.values()
|
||||
]
|
||||
|
||||
return {
|
||||
"entry": async_redact_data(entry.as_dict(), TO_REDACT),
|
||||
"device_info": {
|
||||
"last_update success": coordinator.last_update_success,
|
||||
"last_exception": repr(coordinator.last_exception),
|
||||
"devices": devices,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: AmazonConfigEntry, device_entry: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
assert device_entry.serial_number
|
||||
|
||||
return build_device_data(coordinator.data[device_entry.serial_number])
|
||||
|
||||
|
||||
def build_device_data(device: AmazonDevice) -> dict[str, Any]:
|
||||
"""Build device data for diagnostics."""
|
||||
return {
|
||||
"account name": device.account_name,
|
||||
"capabilities": device.capabilities,
|
||||
"device family": device.device_family,
|
||||
"device type": device.device_type,
|
||||
"device cluster members": device.device_cluster_members,
|
||||
"online": device.online,
|
||||
"serial number": device.serial_number,
|
||||
"software version": device.software_version,
|
||||
"do not disturb": device.do_not_disturb,
|
||||
"response style": device.response_style,
|
||||
"bluetooth state": device.bluetooth_state,
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AmazonDevicesCoordinator
|
||||
|
||||
|
||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: AmazonDevicesCoordinator,
|
||||
serial_num: str,
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._serial_num = serial_num
|
||||
model_details = coordinator.api.get_model_details(self.device) or {}
|
||||
model = model_details.get("model")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@property
|
||||
def device(self) -> AmazonDevice:
|
||||
"""Return the device."""
|
||||
return self.coordinator.data[self._serial_num]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return (
|
||||
super().available
|
||||
and self._serial_num in self.coordinator.data
|
||||
and self.device.online
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"default": "mdi:bluetooth",
|
||||
"state": {
|
||||
"off": "mdi:bluetooth-off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"domain": "alexa_devices",
|
||||
"name": "Alexa Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.0.6"]
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
"""Support for notification entity."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice, AmazonEchoApi
|
||||
|
||||
from homeassistant.components.notify import NotifyEntity, NotifyEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Alexa Devices notify entity description."""
|
||||
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
|
||||
|
||||
NOTIFY: Final = (
|
||||
AmazonNotifyEntityDescription(
|
||||
key="speak",
|
||||
translation_key="speak",
|
||||
subkey="AUDIO_PLAYER",
|
||||
method=lambda api, device, message: api.call_alexa_speak(device, message),
|
||||
),
|
||||
AmazonNotifyEntityDescription(
|
||||
key="announce",
|
||||
translation_key="announce",
|
||||
subkey="AUDIO_PLAYER",
|
||||
method=lambda api, device, message: api.call_alexa_announcement(
|
||||
device, message
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices notification entity based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonNotifyEntity(coordinator, serial_num, sensor_desc)
|
||||
for sensor_desc in NOTIFY
|
||||
for serial_num in coordinator.data
|
||||
if sensor_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
)
|
||||
|
||||
|
||||
class AmazonNotifyEntity(AmazonEntity, NotifyEntity):
|
||||
"""Binary sensor notify platform."""
|
||||
|
||||
entity_description: AmazonNotifyEntityDescription
|
||||
|
||||
async def async_send_message(
|
||||
self, message: str, title: str | None = None, **kwargs: Any
|
||||
) -> None:
|
||||
"""Send a message."""
|
||||
|
||||
await self.entity_description.method(self.coordinator.api, self.device, message)
|
||||
@@ -1,76 +0,0 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: no actions
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: entities do not explicitly subscribe to events
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: all tests missing
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: Network information not relevant
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: There are a ton of mac address ranges in use, but also by kindles which are not supported by this integration
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: no known use cases for repair issues or flows, yet
|
||||
stale-devices:
|
||||
status: todo
|
||||
comment: automate the cleanup process
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: done
|
||||
@@ -1,60 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"data_country": "Country code",
|
||||
"data_code": "One-time password (OTP code)",
|
||||
"data_description_country": "The country of your Amazon account.",
|
||||
"data_description_username": "The email address of your Amazon account.",
|
||||
"data_description_password": "The password of your Amazon account.",
|
||||
"data_description_code": "The one-time password to log in to your account. Currently, only tokens from OTP applications are supported."
|
||||
},
|
||||
"config": {
|
||||
"flow_title": "{username}",
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"bluetooth": {
|
||||
"name": "Bluetooth"
|
||||
}
|
||||
},
|
||||
"notify": {
|
||||
"speak": {
|
||||
"name": "Speak"
|
||||
},
|
||||
"announce": {
|
||||
"name": "Announce"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"do_not_disturb": {
|
||||
"name": "Do not disturb"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
"""Support for switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import AmazonConfigEntry
|
||||
from .entity import AmazonEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Alexa Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
method: str
|
||||
|
||||
|
||||
SWITCHES: Final = (
|
||||
AmazonSwitchEntityDescription(
|
||||
key="do_not_disturb",
|
||||
subkey="AUDIO_PLAYER",
|
||||
translation_key="do_not_disturb",
|
||||
is_on_fn=lambda _device: _device.do_not_disturb,
|
||||
method="set_do_not_disturb",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices switches based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AmazonSwitchEntity(coordinator, serial_num, switch_desc)
|
||||
for switch_desc in SWITCHES
|
||||
for serial_num in coordinator.data
|
||||
if switch_desc.subkey in coordinator.data[serial_num].capabilities
|
||||
)
|
||||
|
||||
|
||||
class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
"""Switch device."""
|
||||
|
||||
entity_description: AmazonSwitchEntityDescription
|
||||
|
||||
async def _switch_set_state(self, state: bool) -> None:
|
||||
"""Set desired switch state."""
|
||||
method = getattr(self.coordinator.api, self.entity_description.method)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self._switch_set_state(True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
await self._switch_set_state(False)
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if switch is on."""
|
||||
return self.entity_description.is_on_fn(self.device)
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"requirements": ["androidtvremote2==0.2.2"],
|
||||
"requirements": ["androidtvremote2==0.2.1"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -51,10 +51,6 @@
|
||||
"app_id": "Application ID",
|
||||
"app_icon": "Application icon",
|
||||
"app_delete": "Check to delete this application"
|
||||
},
|
||||
"data_description": {
|
||||
"app_id": "E.g. com.plexapp.android for https://play.google.com/store/apps/details?id=com.plexapp.android",
|
||||
"app_icon": "Image URL. From the Play Store app page, right click on the icon and select 'Copy image address' and then paste it here. Alternatively, download the image, upload it under /config/www/ and use the URL /local/filename"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,11 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(step_id="user", data_schema=_SCHEMA)
|
||||
|
||||
host, port = user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
|
||||
# Abort if an entry with same host and port is present.
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
|
||||
# Test the connection to the host and get the current status for serial number.
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
@@ -63,30 +67,3 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
title = data.name or data.model or data.serial_no or "APC UPS"
|
||||
return self.async_create_entry(title=title, data=user_input)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry."""
|
||||
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="reconfigure", data_schema=_SCHEMA)
|
||||
|
||||
host, port = user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
self._async_abort_entries_match({CONF_HOST: host, CONF_PORT: port})
|
||||
try:
|
||||
async with asyncio.timeout(CONNECTION_TIMEOUT):
|
||||
data = APCUPSdData(await aioapcaccess.request_status(host, port))
|
||||
except (OSError, asyncio.IncompleteReadError, TimeoutError):
|
||||
errors = {"base": "cannot_connect"}
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
await self.async_set_unique_id(data.serial_no)
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_apcupsd_daemon")
|
||||
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates=user_input,
|
||||
)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"wrong_apcupsd_daemon": "The reconfigured APC UPS Daemon is not the same as the one already configured.",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
|
||||
@@ -62,8 +62,6 @@ async def async_setup_entry(
|
||||
target_humidity_key=Attribute.HUMIDIFICATION_SETPOINT,
|
||||
min_humidity=10,
|
||||
max_humidity=50,
|
||||
auto_status_key=Attribute.HUMIDIFICATION_AVAILABLE,
|
||||
auto_status_value=1,
|
||||
default_humidity=30,
|
||||
set_humidity_fn=coordinator.client.set_humidification_setpoint,
|
||||
)
|
||||
@@ -79,8 +77,6 @@ async def async_setup_entry(
|
||||
action_map=DEHUMIDIFIER_ACTION_MAP,
|
||||
current_humidity_key=Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE,
|
||||
target_humidity_key=Attribute.DEHUMIDIFICATION_SETPOINT,
|
||||
auto_status_key=None,
|
||||
auto_status_value=None,
|
||||
min_humidity=40,
|
||||
max_humidity=90,
|
||||
default_humidity=60,
|
||||
@@ -104,8 +100,6 @@ class AprilaireHumidifierDescription(HumidifierEntityDescription):
|
||||
target_humidity_key: str
|
||||
min_humidity: int
|
||||
max_humidity: int
|
||||
auto_status_key: str | None
|
||||
auto_status_value: int | None
|
||||
default_humidity: int
|
||||
set_humidity_fn: Callable[[int], Awaitable]
|
||||
|
||||
@@ -169,31 +163,14 @@ class AprilaireHumidifierEntity(BaseAprilaireEntity, HumidifierEntity):
|
||||
def min_humidity(self) -> float:
|
||||
"""Return the minimum humidity."""
|
||||
|
||||
if self.is_auto_humidity_mode():
|
||||
return 1
|
||||
|
||||
return self.entity_description.min_humidity
|
||||
|
||||
@property
|
||||
def max_humidity(self) -> float:
|
||||
"""Return the maximum humidity."""
|
||||
|
||||
if self.is_auto_humidity_mode():
|
||||
return 7
|
||||
|
||||
return self.entity_description.max_humidity
|
||||
|
||||
def is_auto_humidity_mode(self) -> bool:
|
||||
"""Return whether the humidifier is in auto mode."""
|
||||
|
||||
if self.entity_description.auto_status_key is None:
|
||||
return False
|
||||
|
||||
return (
|
||||
self.coordinator.data.get(self.entity_description.auto_status_key)
|
||||
== self.entity_description.auto_status_value
|
||||
)
|
||||
|
||||
async def async_set_humidity(self, humidity: int) -> None:
|
||||
"""Set the humidity."""
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyaprilaire"],
|
||||
"requirements": ["pyaprilaire==0.9.1"]
|
||||
"requirements": ["pyaprilaire==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["APsystemsEZ1"],
|
||||
"requirements": ["apsystems-ez1==2.7.0"]
|
||||
"requirements": ["apsystems-ez1==2.6.0"]
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"last_update": {
|
||||
"default": "mdi:update"
|
||||
},
|
||||
"salt_left_side_percentage": {
|
||||
"default": "mdi:basket-fill"
|
||||
},
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from aioaquacell import Softener
|
||||
|
||||
@@ -29,7 +28,7 @@ PARALLEL_UPDATES = 1
|
||||
class SoftenerSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes Softener sensor entity."""
|
||||
|
||||
value_fn: Callable[[Softener], StateType | datetime]
|
||||
value_fn: Callable[[Softener], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
|
||||
@@ -78,12 +77,6 @@ SENSORS: tuple[SoftenerSensorEntityDescription, ...] = (
|
||||
"low",
|
||||
],
|
||||
),
|
||||
SoftenerSensorEntityDescription(
|
||||
key="last_update",
|
||||
translation_key="last_update",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=lambda softener: softener.lastUpdate,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -118,6 +111,6 @@ class SoftenerSensor(AquacellEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime:
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.softener)
|
||||
|
||||
@@ -21,9 +21,6 @@
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"last_update": {
|
||||
"name": "Last update"
|
||||
},
|
||||
"salt_left_side_percentage": {
|
||||
"name": "Salt left side percentage"
|
||||
},
|
||||
|
||||
@@ -1178,33 +1178,25 @@ class PipelineRun:
|
||||
if role := delta.get("role"):
|
||||
chat_log_role = role
|
||||
|
||||
# We are only interested in assistant deltas
|
||||
if chat_log_role != "assistant":
|
||||
# We are only interested in assistant deltas with content
|
||||
if chat_log_role != "assistant" or not (
|
||||
content := delta.get("content")
|
||||
):
|
||||
return
|
||||
|
||||
if content := delta.get("content"):
|
||||
tts_input_stream.put_nowait(content)
|
||||
tts_input_stream.put_nowait(content)
|
||||
|
||||
if self._streamed_response_text:
|
||||
return
|
||||
|
||||
nonlocal delta_character_count
|
||||
|
||||
# Streamed responses are not cached. That's why we only start streaming text after
|
||||
# we have received enough characters that indicates it will be a long response
|
||||
# or if we have received text, and then a tool call.
|
||||
|
||||
# Tool call after we already received text
|
||||
start_streaming = delta_character_count > 0 and delta.get("tool_calls")
|
||||
|
||||
# Count characters in the content and test if we exceed streaming threshold
|
||||
if not start_streaming and content:
|
||||
delta_character_count += len(content)
|
||||
start_streaming = delta_character_count > STREAM_RESPONSE_CHARS
|
||||
|
||||
if not start_streaming:
|
||||
delta_character_count += len(content)
|
||||
if delta_character_count < STREAM_RESPONSE_CHARS:
|
||||
return
|
||||
|
||||
# Streamed responses are not cached. We only start streaming text after
|
||||
# we have received a couple of words that indicates it will be a long response.
|
||||
self._streamed_response_text = True
|
||||
|
||||
async def tts_input_stream_generator() -> AsyncGenerator[str]:
|
||||
@@ -1212,17 +1204,6 @@ class PipelineRun:
|
||||
while (tts_input := await tts_input_stream.get()) is not None:
|
||||
yield tts_input
|
||||
|
||||
# Concatenate all existing queue items
|
||||
parts = []
|
||||
while not tts_input_stream.empty():
|
||||
parts.append(tts_input_stream.get_nowait())
|
||||
tts_input_stream.put_nowait(
|
||||
"".join(
|
||||
# At this point parts is only strings, None indicates end of queue
|
||||
cast(list[str], parts)
|
||||
)
|
||||
)
|
||||
|
||||
assert self.tts_stream is not None
|
||||
self.tts_stream.async_set_message_stream(tts_input_stream_generator())
|
||||
|
||||
|
||||
@@ -62,7 +62,6 @@ from .const import (
|
||||
LOGGER,
|
||||
)
|
||||
from .models import (
|
||||
AddonInfo,
|
||||
AgentBackup,
|
||||
BackupError,
|
||||
BackupManagerError,
|
||||
@@ -103,9 +102,7 @@ class ManagerBackup(BaseBackup):
|
||||
"""Backup class."""
|
||||
|
||||
agents: dict[str, AgentBackupStatus]
|
||||
failed_addons: list[AddonInfo]
|
||||
failed_agent_ids: list[str]
|
||||
failed_folders: list[Folder]
|
||||
with_automatic_settings: bool | None
|
||||
|
||||
|
||||
@@ -113,7 +110,7 @@ class ManagerBackup(BaseBackup):
|
||||
class AddonErrorData:
|
||||
"""Addon error class."""
|
||||
|
||||
addon: AddonInfo
|
||||
name: str
|
||||
errors: list[tuple[str, str]]
|
||||
|
||||
|
||||
@@ -649,13 +646,9 @@ class BackupManager:
|
||||
for agent_backup in result:
|
||||
if (backup_id := agent_backup.backup_id) not in backups:
|
||||
if known_backup := self.known_backups.get(backup_id):
|
||||
failed_addons = known_backup.failed_addons
|
||||
failed_agent_ids = known_backup.failed_agent_ids
|
||||
failed_folders = known_backup.failed_folders
|
||||
else:
|
||||
failed_addons = []
|
||||
failed_agent_ids = []
|
||||
failed_folders = []
|
||||
with_automatic_settings = self.is_our_automatic_backup(
|
||||
agent_backup, await instance_id.async_get(self.hass)
|
||||
)
|
||||
@@ -666,9 +659,7 @@ class BackupManager:
|
||||
date=agent_backup.date,
|
||||
database_included=agent_backup.database_included,
|
||||
extra_metadata=agent_backup.extra_metadata,
|
||||
failed_addons=failed_addons,
|
||||
failed_agent_ids=failed_agent_ids,
|
||||
failed_folders=failed_folders,
|
||||
folders=agent_backup.folders,
|
||||
homeassistant_included=agent_backup.homeassistant_included,
|
||||
homeassistant_version=agent_backup.homeassistant_version,
|
||||
@@ -723,13 +714,9 @@ class BackupManager:
|
||||
continue
|
||||
if backup is None:
|
||||
if known_backup := self.known_backups.get(backup_id):
|
||||
failed_addons = known_backup.failed_addons
|
||||
failed_agent_ids = known_backup.failed_agent_ids
|
||||
failed_folders = known_backup.failed_folders
|
||||
else:
|
||||
failed_addons = []
|
||||
failed_agent_ids = []
|
||||
failed_folders = []
|
||||
with_automatic_settings = self.is_our_automatic_backup(
|
||||
result, await instance_id.async_get(self.hass)
|
||||
)
|
||||
@@ -740,9 +727,7 @@ class BackupManager:
|
||||
date=result.date,
|
||||
database_included=result.database_included,
|
||||
extra_metadata=result.extra_metadata,
|
||||
failed_addons=failed_addons,
|
||||
failed_agent_ids=failed_agent_ids,
|
||||
failed_folders=failed_folders,
|
||||
folders=result.folders,
|
||||
homeassistant_included=result.homeassistant_included,
|
||||
homeassistant_version=result.homeassistant_version,
|
||||
@@ -985,7 +970,7 @@ class BackupManager:
|
||||
password=None,
|
||||
)
|
||||
await written_backup.release_stream()
|
||||
self.known_backups.add(written_backup.backup, agent_errors, {}, {}, [])
|
||||
self.known_backups.add(written_backup.backup, agent_errors, [])
|
||||
return written_backup.backup.backup_id
|
||||
|
||||
async def async_create_backup(
|
||||
@@ -1223,11 +1208,7 @@ class BackupManager:
|
||||
finally:
|
||||
await written_backup.release_stream()
|
||||
self.known_backups.add(
|
||||
written_backup.backup,
|
||||
agent_errors,
|
||||
written_backup.addon_errors,
|
||||
written_backup.folder_errors,
|
||||
unavailable_agents,
|
||||
written_backup.backup, agent_errors, unavailable_agents
|
||||
)
|
||||
if not agent_errors:
|
||||
if with_automatic_settings:
|
||||
@@ -1435,12 +1416,7 @@ class BackupManager:
|
||||
# No issues with agents or folders, but issues with add-ons
|
||||
self._create_automatic_backup_failed_issue(
|
||||
"automatic_backup_failed_addons",
|
||||
{
|
||||
"failed_addons": ", ".join(
|
||||
val.addon.name or val.addon.slug
|
||||
for val in addon_errors.values()
|
||||
)
|
||||
},
|
||||
{"failed_addons": ", ".join(val.name for val in addon_errors.values())},
|
||||
)
|
||||
elif folder_errors and not (failed_agents or addon_errors):
|
||||
# No issues with agents or add-ons, but issues with folders
|
||||
@@ -1455,11 +1431,7 @@ class BackupManager:
|
||||
{
|
||||
"failed_agents": ", ".join(failed_agents) or "-",
|
||||
"failed_addons": (
|
||||
", ".join(
|
||||
val.addon.name or val.addon.slug
|
||||
for val in addon_errors.values()
|
||||
)
|
||||
or "-"
|
||||
", ".join(val.name for val in addon_errors.values()) or "-"
|
||||
),
|
||||
"failed_folders": ", ".join(f for f in folder_errors) or "-",
|
||||
},
|
||||
@@ -1529,12 +1501,7 @@ class KnownBackups:
|
||||
self._backups = {
|
||||
backup["backup_id"]: KnownBackup(
|
||||
backup_id=backup["backup_id"],
|
||||
failed_addons=[
|
||||
AddonInfo(name=a["name"], slug=a["slug"], version=a["version"])
|
||||
for a in backup["failed_addons"]
|
||||
],
|
||||
failed_agent_ids=backup["failed_agent_ids"],
|
||||
failed_folders=[Folder(f) for f in backup["failed_folders"]],
|
||||
)
|
||||
for backup in stored_backups
|
||||
}
|
||||
@@ -1547,16 +1514,12 @@ class KnownBackups:
|
||||
self,
|
||||
backup: AgentBackup,
|
||||
agent_errors: dict[str, Exception],
|
||||
failed_addons: dict[str, AddonErrorData],
|
||||
failed_folders: dict[Folder, list[tuple[str, str]]],
|
||||
unavailable_agents: list[str],
|
||||
) -> None:
|
||||
"""Add a backup."""
|
||||
self._backups[backup.backup_id] = KnownBackup(
|
||||
backup_id=backup.backup_id,
|
||||
failed_addons=[val.addon for val in failed_addons.values()],
|
||||
failed_agent_ids=list(chain(agent_errors, unavailable_agents)),
|
||||
failed_folders=list(failed_folders),
|
||||
)
|
||||
self._manager.store.save()
|
||||
|
||||
@@ -1577,38 +1540,21 @@ class KnownBackup:
|
||||
"""Persistent backup data."""
|
||||
|
||||
backup_id: str
|
||||
failed_addons: list[AddonInfo]
|
||||
failed_agent_ids: list[str]
|
||||
failed_folders: list[Folder]
|
||||
|
||||
def to_dict(self) -> StoredKnownBackup:
|
||||
"""Convert known backup to a dict."""
|
||||
return {
|
||||
"backup_id": self.backup_id,
|
||||
"failed_addons": [
|
||||
{"name": a.name, "slug": a.slug, "version": a.version}
|
||||
for a in self.failed_addons
|
||||
],
|
||||
"failed_agent_ids": self.failed_agent_ids,
|
||||
"failed_folders": [f.value for f in self.failed_folders],
|
||||
}
|
||||
|
||||
|
||||
class StoredAddonInfo(TypedDict):
|
||||
"""Stored add-on info."""
|
||||
|
||||
name: str | None
|
||||
slug: str
|
||||
version: str | None
|
||||
|
||||
|
||||
class StoredKnownBackup(TypedDict):
|
||||
"""Stored persistent backup data."""
|
||||
|
||||
backup_id: str
|
||||
failed_addons: list[StoredAddonInfo]
|
||||
failed_agent_ids: list[str]
|
||||
failed_folders: list[str]
|
||||
|
||||
|
||||
class CoreBackupReaderWriter(BackupReaderWriter):
|
||||
|
||||
@@ -13,9 +13,9 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
class AddonInfo:
|
||||
"""Addon information."""
|
||||
|
||||
name: str | None
|
||||
name: str
|
||||
slug: str
|
||||
version: str | None
|
||||
version: str
|
||||
|
||||
|
||||
class Folder(StrEnum):
|
||||
|
||||
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
|
||||
STORE_DELAY_SAVE = 30
|
||||
STORAGE_KEY = DOMAIN
|
||||
STORAGE_VERSION = 1
|
||||
STORAGE_VERSION_MINOR = 7
|
||||
STORAGE_VERSION_MINOR = 6
|
||||
|
||||
|
||||
class StoredBackupData(TypedDict):
|
||||
@@ -76,11 +76,6 @@ class _BackupStore(Store[StoredBackupData]):
|
||||
# Version 1.6 adds agent retention settings
|
||||
for agent in data["config"]["agents"]:
|
||||
data["config"]["agents"][agent]["retention"] = None
|
||||
if old_minor_version < 7:
|
||||
# Version 1.7 adds failing addons and folders
|
||||
for backup in data["backups"]:
|
||||
backup["failed_addons"] = []
|
||||
backup["failed_folders"] = []
|
||||
|
||||
# Note: We allow reading data with major version 2 in which the unused key
|
||||
# data["config"]["schedule"]["state"] will be removed. The bump to 2 is
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.2",
|
||||
"bluetooth-data-tools==1.28.1",
|
||||
"dbus-fast==2.43.0",
|
||||
"habluetooth==3.49.0"
|
||||
"habluetooth==3.48.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
|
||||
|
||||
def __init__(self, panel: Panel, area_id: int, unique_id: str) -> None:
|
||||
"""Initialise a Bosch Alarm control panel entity."""
|
||||
super().__init__(panel, area_id, unique_id, True, False, True)
|
||||
super().__init__(panel, area_id, unique_id, False, False, True)
|
||||
self._attr_unique_id = self._area_unique_id
|
||||
|
||||
@property
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/caldav",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["caldav", "vobject"],
|
||||
"requirements": ["caldav==1.6.0", "icalendar==6.1.0"]
|
||||
"requirements": ["caldav==1.3.9", "icalendar==6.1.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
ClimateEntity,
|
||||
ClimateEntityFeature,
|
||||
HVACAction,
|
||||
@@ -18,12 +17,13 @@ from homeassistant.components.climate import (
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import PRESET_MODE_AUTO, PRESET_MODE_AUTO_TARGET_TEMP, PRESET_MODE_MANUAL
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call, cleanup_stale_entity, load_api_data
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -41,13 +41,11 @@ class ClimaComelitMode(StrEnum):
|
||||
class ClimaComelitCommand(StrEnum):
|
||||
"""Serial Bridge clima commands."""
|
||||
|
||||
AUTO = "auto"
|
||||
MANUAL = "man"
|
||||
OFF = "off"
|
||||
ON = "on"
|
||||
MANUAL = "man"
|
||||
SET = "set"
|
||||
SNOW = "lower"
|
||||
SUN = "upper"
|
||||
AUTO = "auto"
|
||||
|
||||
|
||||
class ClimaComelitApiStatus(TypedDict):
|
||||
@@ -69,15 +67,11 @@ API_STATUS: dict[str, ClimaComelitApiStatus] = {
|
||||
),
|
||||
}
|
||||
|
||||
HVACMODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = {
|
||||
MODE_TO_ACTION: dict[HVACMode, ClimaComelitCommand] = {
|
||||
HVACMode.OFF: ClimaComelitCommand.OFF,
|
||||
HVACMode.COOL: ClimaComelitCommand.SNOW,
|
||||
HVACMode.HEAT: ClimaComelitCommand.SUN,
|
||||
}
|
||||
|
||||
PRESET_MODE_TO_ACTION: dict[str, ClimaComelitCommand] = {
|
||||
PRESET_MODE_MANUAL: ClimaComelitCommand.MANUAL,
|
||||
PRESET_MODE_AUTO: ClimaComelitCommand.AUTO,
|
||||
HVACMode.AUTO: ClimaComelitCommand.AUTO,
|
||||
HVACMode.COOL: ClimaComelitCommand.MANUAL,
|
||||
HVACMode.HEAT: ClimaComelitCommand.MANUAL,
|
||||
}
|
||||
|
||||
|
||||
@@ -90,42 +84,26 @@ async def async_setup_entry(
|
||||
|
||||
coordinator = cast(ComelitSerialBridge, config_entry.runtime_data)
|
||||
|
||||
entities: list[ClimateEntity] = []
|
||||
for device in coordinator.data[CLIMATE].values():
|
||||
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||
if values[0] == 0 and values[4] == 0:
|
||||
# No climate data, device is only a humidifier/dehumidifier
|
||||
|
||||
await cleanup_stale_entity(
|
||||
hass, config_entry, f"{config_entry.entry_id}-{device.index}", device
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
entities.append(
|
||||
ComelitClimateEntity(coordinator, device, config_entry.entry_id)
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
ComelitClimateEntity(coordinator, device, config_entry.entry_id)
|
||||
for device in coordinator.data[CLIMATE].values()
|
||||
)
|
||||
|
||||
|
||||
class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
"""Climate device."""
|
||||
|
||||
_attr_hvac_modes = [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_preset_modes = [PRESET_MODE_AUTO, PRESET_MODE_MANUAL]
|
||||
_attr_hvac_modes = [HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF]
|
||||
_attr_max_temp = 30
|
||||
_attr_min_temp = 5
|
||||
_attr_supported_features = (
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
| ClimateEntityFeature.TURN_OFF
|
||||
| ClimateEntityFeature.TURN_ON
|
||||
| ClimateEntityFeature.PRESET_MODE
|
||||
)
|
||||
_attr_target_temperature_step = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_name = None
|
||||
_attr_translation_key = "thermostat"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -140,14 +118,20 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
def _update_attributes(self) -> None:
|
||||
"""Update class attributes."""
|
||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||
values = load_api_data(device, CLIMATE_DOMAIN)
|
||||
if not isinstance(device.val, list):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_clima_data"
|
||||
)
|
||||
|
||||
# CLIMATE has a 2 item tuple:
|
||||
# - first for Clima
|
||||
# - second for Humidifier
|
||||
values = device.val[0]
|
||||
|
||||
_active = values[1]
|
||||
_mode = values[2] # Values from API: "O", "L", "U"
|
||||
_automatic = values[3] == ClimaComelitMode.AUTO
|
||||
|
||||
self._attr_preset_mode = PRESET_MODE_AUTO if _automatic else PRESET_MODE_MANUAL
|
||||
|
||||
self._attr_current_temperature = values[0] / 10
|
||||
|
||||
self._attr_hvac_action = None
|
||||
@@ -157,6 +141,10 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
|
||||
|
||||
self._attr_hvac_mode = None
|
||||
if _mode == ClimaComelitMode.OFF:
|
||||
self._attr_hvac_mode = HVACMode.OFF
|
||||
if _automatic:
|
||||
self._attr_hvac_mode = HVACMode.AUTO
|
||||
if _mode in API_STATUS:
|
||||
self._attr_hvac_mode = API_STATUS[_mode]["hvac_mode"]
|
||||
|
||||
@@ -172,12 +160,13 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
if (
|
||||
(target_temp := kwargs.get(ATTR_TEMPERATURE)) is None
|
||||
or self.hvac_mode == HVACMode.OFF
|
||||
or self._attr_preset_mode == PRESET_MODE_AUTO
|
||||
):
|
||||
target_temp := kwargs.get(ATTR_TEMPERATURE)
|
||||
) is None or self.hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaComelitCommand.MANUAL
|
||||
)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaComelitCommand.SET, target_temp
|
||||
)
|
||||
@@ -188,28 +177,12 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set hvac mode."""
|
||||
|
||||
if self._attr_hvac_mode == HVACMode.OFF:
|
||||
if hvac_mode != HVACMode.OFF:
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, ClimaComelitCommand.ON
|
||||
)
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, HVACMODE_TO_ACTION[hvac_mode]
|
||||
self._device.index, MODE_TO_ACTION[hvac_mode]
|
||||
)
|
||||
self._attr_hvac_mode = hvac_mode
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new target preset mode."""
|
||||
|
||||
if self._attr_hvac_mode == HVACMode.OFF:
|
||||
return
|
||||
|
||||
await self.coordinator.api.set_clima_status(
|
||||
self._device.index, PRESET_MODE_TO_ACTION[preset_mode]
|
||||
)
|
||||
self._attr_preset_mode = preset_mode
|
||||
|
||||
if preset_mode == PRESET_MODE_AUTO:
|
||||
self._attr_target_temperature = PRESET_MODE_AUTO_TARGET_TEMP
|
||||
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -11,8 +11,3 @@ DEFAULT_PORT = 80
|
||||
DEVICE_TYPE_LIST = [BRIDGE, VEDO]
|
||||
|
||||
SCAN_INTERVAL = 5
|
||||
|
||||
PRESET_MODE_AUTO = "automatic"
|
||||
PRESET_MODE_MANUAL = "manual"
|
||||
|
||||
PRESET_MODE_AUTO_TARGET_TEMP = 20
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.const import CLIMATE
|
||||
|
||||
from homeassistant.components.humidifier import (
|
||||
DOMAIN as HUMIDIFIER_DOMAIN,
|
||||
MODE_AUTO,
|
||||
MODE_NORMAL,
|
||||
HumidifierAction,
|
||||
@@ -18,13 +17,13 @@ from homeassistant.components.humidifier import (
|
||||
HumidifierEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ComelitConfigEntry, ComelitSerialBridge
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
from .utils import bridge_api_call, cleanup_stale_entity, load_api_data
|
||||
from .utils import bridge_api_call
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -68,23 +67,6 @@ async def async_setup_entry(
|
||||
|
||||
entities: list[ComelitHumidifierEntity] = []
|
||||
for device in coordinator.data[CLIMATE].values():
|
||||
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||
if values[0] == 0 and values[4] == 0:
|
||||
# No humidity data, device is only a climate
|
||||
|
||||
for device_class in (
|
||||
HumidifierDeviceClass.HUMIDIFIER,
|
||||
HumidifierDeviceClass.DEHUMIDIFIER,
|
||||
):
|
||||
await cleanup_stale_entity(
|
||||
hass,
|
||||
config_entry,
|
||||
f"{config_entry.entry_id}-{device.index}-{device_class}",
|
||||
device,
|
||||
)
|
||||
|
||||
continue
|
||||
|
||||
entities.append(
|
||||
ComelitHumidifierEntity(
|
||||
coordinator,
|
||||
@@ -142,7 +124,15 @@ class ComelitHumidifierEntity(ComelitBridgeBaseEntity, HumidifierEntity):
|
||||
def _update_attributes(self) -> None:
|
||||
"""Update class attributes."""
|
||||
device = self.coordinator.data[CLIMATE][self._device.index]
|
||||
values = load_api_data(device, HUMIDIFIER_DOMAIN)
|
||||
if not isinstance(device.val, list):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_clima_data"
|
||||
)
|
||||
|
||||
# CLIMATE has a 2 item tuple:
|
||||
# - first for Clima
|
||||
# - second for Humidifier
|
||||
values = device.val[1]
|
||||
|
||||
_active = values[1]
|
||||
_mode = values[2] # Values from API: "O", "L", "U"
|
||||
|
||||
@@ -4,18 +4,6 @@
|
||||
"zone_status": {
|
||||
"default": "mdi:shield-check"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"automatic": "mdi:refresh-auto",
|
||||
"manual": "mdi:alpha-m"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,18 +74,6 @@
|
||||
"dehumidifier": {
|
||||
"name": "Dehumidifier"
|
||||
}
|
||||
},
|
||||
"climate": {
|
||||
"thermostat": {
|
||||
"state_attributes": {
|
||||
"preset_mode": {
|
||||
"state": {
|
||||
"automatic": "[%key:common::state::auto%]",
|
||||
"manual": "[%key:common::state::manual%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -4,21 +4,14 @@ from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from aiocomelit import ComelitSerialBridgeObject
|
||||
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
|
||||
from aiohttp import ClientSession, CookieJar
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import _LOGGER, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .entity import ComelitBridgeBaseEntity
|
||||
|
||||
|
||||
@@ -29,61 +22,6 @@ async def async_client_session(hass: HomeAssistant) -> ClientSession:
|
||||
)
|
||||
|
||||
|
||||
def load_api_data(device: ComelitSerialBridgeObject, domain: str) -> list[Any]:
|
||||
"""Load data from the API."""
|
||||
# This function is called when the data is loaded from the API
|
||||
if not isinstance(device.val, list):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=domain, translation_key="invalid_clima_data"
|
||||
)
|
||||
# CLIMATE has a 2 item tuple:
|
||||
# - first for Clima
|
||||
# - second for Humidifier
|
||||
return device.val[0] if domain == CLIMATE_DOMAIN else device.val[1]
|
||||
|
||||
|
||||
async def cleanup_stale_entity(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
entry_unique_id: str,
|
||||
device: ComelitSerialBridgeObject,
|
||||
) -> None:
|
||||
"""Cleanup stale entity."""
|
||||
entity_reg: er.EntityRegistry = er.async_get(hass)
|
||||
|
||||
identifiers: list[str] = []
|
||||
|
||||
for entry in er.async_entries_for_config_entry(entity_reg, config_entry.entry_id):
|
||||
if entry.unique_id == entry_unique_id:
|
||||
entry_name = entry.name or entry.original_name
|
||||
_LOGGER.info("Removing entity: %s [%s]", entry.entity_id, entry_name)
|
||||
entity_reg.async_remove(entry.entity_id)
|
||||
identifiers.append(f"{config_entry.entry_id}-{device.type}-{device.index}")
|
||||
|
||||
if len(identifiers) > 0:
|
||||
_async_remove_state_config_entry_from_devices(hass, identifiers, config_entry)
|
||||
|
||||
|
||||
def _async_remove_state_config_entry_from_devices(
|
||||
hass: HomeAssistant, identifiers: list[str], config_entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Remove config entry from device."""
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
for identifier in identifiers:
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, identifier)})
|
||||
if device:
|
||||
_LOGGER.info(
|
||||
"Removing config entry %s from device %s",
|
||||
config_entry.title,
|
||||
device.name,
|
||||
)
|
||||
device_registry.async_update_device(
|
||||
device_id=device.id,
|
||||
remove_config_entry_id=config_entry.entry_id,
|
||||
)
|
||||
|
||||
|
||||
def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
|
||||
func: Callable[Concatenate[_T, _P], Awaitable[None]],
|
||||
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
|
||||
|
||||
@@ -9,12 +9,10 @@ from typing import Any
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
from homeassistant.const import CONF_COMMAND
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.process import kill_subprocess
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .const import CONF_COMMAND_TIMEOUT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -45,31 +43,8 @@ class CommandLineNotificationService(BaseNotificationService):
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a command line."""
|
||||
command = self.command
|
||||
if " " not in command:
|
||||
prog = command
|
||||
args = None
|
||||
args_compiled = None
|
||||
else:
|
||||
prog, args = command.split(" ", 1)
|
||||
args_compiled = Template(args, self.hass)
|
||||
|
||||
rendered_args = None
|
||||
if args_compiled:
|
||||
args_to_render = {"arguments": args}
|
||||
try:
|
||||
rendered_args = args_compiled.async_render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
|
||||
if rendered_args != args:
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
LOGGER.debug("Running command: %s, with message: %s", command, message)
|
||||
|
||||
with subprocess.Popen( # noqa: S602 # shell by design
|
||||
command,
|
||||
self.command,
|
||||
universal_newlines=True,
|
||||
stdin=subprocess.PIPE,
|
||||
close_fds=False, # required for posix_spawn
|
||||
@@ -81,10 +56,10 @@ class CommandLineNotificationService(BaseNotificationService):
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s",
|
||||
proc.returncode,
|
||||
command,
|
||||
self.command,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
_LOGGER.error("Timeout for command: %s", self.command)
|
||||
kill_subprocess(proc)
|
||||
except subprocess.SubprocessError:
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
_LOGGER.error("Error trying to exec command: %s", self.command)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
@@ -11,23 +10,18 @@ from homeassistant import config_entries
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
|
||||
from homeassistant.helpers.json import json_dumps
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup(hass: HomeAssistant) -> bool:
|
||||
"""Enable the Entity Registry views."""
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids)
|
||||
websocket_api.async_register_command(hass, websocket_get_entities)
|
||||
websocket_api.async_register_command(hass, websocket_get_entity)
|
||||
websocket_api.async_register_command(hass, websocket_list_entities_for_display)
|
||||
@@ -322,54 +316,3 @@ def websocket_remove_entity(
|
||||
|
||||
registry.async_remove(msg["entity_id"])
|
||||
connection.send_message(websocket_api.result_message(msg["id"]))
|
||||
|
||||
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "config/entity_registry/get_automatic_entity_ids",
|
||||
vol.Required("entity_ids"): cv.entity_ids,
|
||||
}
|
||||
)
|
||||
@callback
|
||||
def websocket_get_automatic_entity_ids(
|
||||
hass: HomeAssistant,
|
||||
connection: websocket_api.ActiveConnection,
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return the automatic entity IDs for the given entity IDs.
|
||||
|
||||
This is used to help user reset entity IDs which have been customized by the user.
|
||||
"""
|
||||
registry = er.async_get(hass)
|
||||
|
||||
entity_ids = msg["entity_ids"]
|
||||
automatic_entity_ids: dict[str, str | None] = {}
|
||||
reserved_entity_ids: set[str] = set()
|
||||
for entity_id in entity_ids:
|
||||
if not (entry := registry.entities.get(entity_id)):
|
||||
automatic_entity_ids[entity_id] = None
|
||||
continue
|
||||
try:
|
||||
suggested = async_get_entity_suggested_object_id(hass, entity_id)
|
||||
except HomeAssistantError as err:
|
||||
# This is raised if the entity has no object.
|
||||
_LOGGER.debug(
|
||||
"Unable to get suggested object ID for %s, entity ID: %s (%s)",
|
||||
entry.entity_id,
|
||||
entity_id,
|
||||
err,
|
||||
)
|
||||
automatic_entity_ids[entity_id] = None
|
||||
continue
|
||||
suggested_entity_id = registry.async_generate_entity_id(
|
||||
entry.domain,
|
||||
suggested or f"{entry.platform}_{entry.unique_id}",
|
||||
current_entity_id=entity_id,
|
||||
reserved_entity_ids=reserved_entity_ids,
|
||||
)
|
||||
automatic_entity_ids[entity_id] = suggested_entity_id
|
||||
reserved_entity_ids.add(suggested_entity_id)
|
||||
|
||||
connection.send_message(
|
||||
websocket_api.result_message(msg["id"], automatic_entity_ids)
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.6.10"]
|
||||
"requirements": ["hassil==2.2.3", "home-assistant-intents==2025.5.7"]
|
||||
}
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
"""The cups component."""
|
||||
|
||||
DOMAIN = "cups"
|
||||
CONF_PRINTERS = "printers"
|
||||
|
||||
@@ -14,15 +14,12 @@ from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, PERCENTAGE
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_PRINTERS, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_MARKER_TYPE = "marker_type"
|
||||
@@ -39,6 +36,7 @@ ATTR_PRINTER_STATE_REASON = "printer_state_reason"
|
||||
ATTR_PRINTER_TYPE = "printer_type"
|
||||
ATTR_PRINTER_URI_SUPPORTED = "printer_uri_supported"
|
||||
|
||||
CONF_PRINTERS = "printers"
|
||||
CONF_IS_CUPS_SERVER = "is_cups_server"
|
||||
|
||||
DEFAULT_HOST = "127.0.0.1"
|
||||
@@ -74,21 +72,6 @@ def setup_platform(
|
||||
printers: list[str] = config[CONF_PRINTERS]
|
||||
is_cups: bool = config[CONF_IS_CUPS_SERVER]
|
||||
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "CUPS",
|
||||
},
|
||||
)
|
||||
|
||||
if is_cups:
|
||||
data = CupsData(host, port, None)
|
||||
data.update()
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
"""The decora component."""
|
||||
|
||||
DOMAIN = "decora"
|
||||
|
||||
@@ -21,11 +21,7 @@ from homeassistant.components.light import (
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_DEVICES, CONF_NAME
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -94,21 +90,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up an Decora switch."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Leviton Decora",
|
||||
},
|
||||
)
|
||||
|
||||
lights = []
|
||||
for address, device_config in config[CONF_DEVICES].items():
|
||||
device = {}
|
||||
|
||||
@@ -6,10 +6,8 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SOURCE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -19,28 +17,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass, entry.entry_id, entry.options[CONF_SOURCE]
|
||||
)
|
||||
|
||||
def set_source_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_source_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_SOURCE]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_SOURCE],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -2,13 +2,27 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import Semaphore
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from devolo_plc_api import Device
|
||||
from devolo_plc_api.exceptions.device import DeviceNotFound
|
||||
from devolo_plc_api.device_api import (
|
||||
ConnectedStationInfo,
|
||||
NeighborAPInfo,
|
||||
UpdateFirmwareCheck,
|
||||
WifiGuestAccessGet,
|
||||
)
|
||||
from devolo_plc_api.exceptions.device import (
|
||||
DeviceNotFound,
|
||||
DevicePasswordProtected,
|
||||
DeviceUnavailable,
|
||||
)
|
||||
from devolo_plc_api.plcnet_api import LogicalNetwork
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_IP_ADDRESS,
|
||||
CONF_PASSWORD,
|
||||
@@ -16,34 +30,38 @@ from homeassistant.const import (
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONNECTED_PLC_DEVICES,
|
||||
CONNECTED_WIFI_CLIENTS,
|
||||
DOMAIN,
|
||||
FIRMWARE_UPDATE_INTERVAL,
|
||||
LAST_RESTART,
|
||||
LONG_UPDATE_INTERVAL,
|
||||
NEIGHBORING_WIFI_NETWORKS,
|
||||
REGULAR_FIRMWARE,
|
||||
SHORT_UPDATE_INTERVAL,
|
||||
SWITCH_GUEST_WIFI,
|
||||
SWITCH_LEDS,
|
||||
)
|
||||
from .coordinator import (
|
||||
DevoloDataUpdateCoordinator,
|
||||
DevoloFirmwareUpdateCoordinator,
|
||||
DevoloHomeNetworkConfigEntry,
|
||||
DevoloHomeNetworkData,
|
||||
DevoloLedSettingsGetCoordinator,
|
||||
DevoloLogicalNetworkCoordinator,
|
||||
DevoloUptimeGetCoordinator,
|
||||
DevoloWifiConnectedStationsGetCoordinator,
|
||||
DevoloWifiGuestAccessGetCoordinator,
|
||||
DevoloWifiNeighborAPsGetCoordinator,
|
||||
)
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData]
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevoloHomeNetworkData:
|
||||
"""The devolo Home Network data."""
|
||||
|
||||
device: Device
|
||||
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: DevoloHomeNetworkConfigEntry
|
||||
@@ -51,6 +69,8 @@ async def async_setup_entry(
|
||||
"""Set up devolo Home Network from a config entry."""
|
||||
zeroconf_instance = await zeroconf.async_get_async_instance(hass)
|
||||
async_client = get_async_client(hass)
|
||||
device_registry = dr.async_get(hass)
|
||||
semaphore = Semaphore(1)
|
||||
|
||||
try:
|
||||
device = Device(
|
||||
@@ -70,52 +90,177 @@ async def async_setup_entry(
|
||||
|
||||
entry.runtime_data = DevoloHomeNetworkData(device=device, coordinators={})
|
||||
|
||||
async def async_update_firmware_available() -> UpdateFirmwareCheck:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_check_firmware_available()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
async def async_update_connected_plc_devices() -> LogicalNetwork:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.plcnet
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.plcnet.async_get_network_overview()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
async def async_update_guest_wifi_status() -> WifiGuestAccessGet:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_get_wifi_guest_access()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
except DevicePasswordProtected as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="password_wrong"
|
||||
) from err
|
||||
|
||||
async def async_update_led_status() -> bool:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_get_led_setting()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
async def async_update_last_restart() -> int:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_uptime()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
except DevicePasswordProtected as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="password_wrong"
|
||||
) from err
|
||||
|
||||
async def async_update_wifi_connected_station() -> list[ConnectedStationInfo]:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_get_wifi_connected_station()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
async def async_update_wifi_neighbor_access_points() -> list[NeighborAPInfo]:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert device.device
|
||||
update_sw_version(device_registry, device)
|
||||
try:
|
||||
return await device.device.async_get_wifi_neighbor_access_points()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
async def disconnect(event: Event) -> None:
|
||||
"""Disconnect from device."""
|
||||
await device.async_disconnect()
|
||||
|
||||
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]] = {}
|
||||
if device.plcnet:
|
||||
coordinators[CONNECTED_PLC_DEVICES] = DevoloLogicalNetworkCoordinator(
|
||||
coordinators[CONNECTED_PLC_DEVICES] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=CONNECTED_PLC_DEVICES,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_connected_plc_devices,
|
||||
update_interval=LONG_UPDATE_INTERVAL,
|
||||
)
|
||||
if device.device and "led" in device.device.features:
|
||||
coordinators[SWITCH_LEDS] = DevoloLedSettingsGetCoordinator(
|
||||
coordinators[SWITCH_LEDS] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=SWITCH_LEDS,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_led_status,
|
||||
update_interval=SHORT_UPDATE_INTERVAL,
|
||||
)
|
||||
if device.device and "restart" in device.device.features:
|
||||
coordinators[LAST_RESTART] = DevoloUptimeGetCoordinator(
|
||||
coordinators[LAST_RESTART] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=LAST_RESTART,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_last_restart,
|
||||
update_interval=SHORT_UPDATE_INTERVAL,
|
||||
)
|
||||
if device.device and "update" in device.device.features:
|
||||
coordinators[REGULAR_FIRMWARE] = DevoloFirmwareUpdateCoordinator(
|
||||
coordinators[REGULAR_FIRMWARE] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=REGULAR_FIRMWARE,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_firmware_available,
|
||||
update_interval=FIRMWARE_UPDATE_INTERVAL,
|
||||
)
|
||||
if device.device and "wifi1" in device.device.features:
|
||||
coordinators[CONNECTED_WIFI_CLIENTS] = (
|
||||
DevoloWifiConnectedStationsGetCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
)
|
||||
)
|
||||
coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloWifiNeighborAPsGetCoordinator(
|
||||
coordinators[CONNECTED_WIFI_CLIENTS] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=CONNECTED_WIFI_CLIENTS,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_wifi_connected_station,
|
||||
update_interval=SHORT_UPDATE_INTERVAL,
|
||||
)
|
||||
coordinators[SWITCH_GUEST_WIFI] = DevoloWifiGuestAccessGetCoordinator(
|
||||
coordinators[NEIGHBORING_WIFI_NETWORKS] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=NEIGHBORING_WIFI_NETWORKS,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_wifi_neighbor_access_points,
|
||||
update_interval=LONG_UPDATE_INTERVAL,
|
||||
)
|
||||
coordinators[SWITCH_GUEST_WIFI] = DevoloDataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=SWITCH_GUEST_WIFI,
|
||||
semaphore=semaphore,
|
||||
update_method=async_update_guest_wifi_status,
|
||||
update_interval=SHORT_UPDATE_INTERVAL,
|
||||
)
|
||||
|
||||
for coordinator in coordinators.values():
|
||||
@@ -158,3 +303,16 @@ def platforms(device: Device) -> set[Platform]:
|
||||
if device.device and "update" in device.device.features:
|
||||
supported_platforms.add(Platform.UPDATE)
|
||||
return supported_platforms
|
||||
|
||||
|
||||
@callback
|
||||
def update_sw_version(device_registry: dr.DeviceRegistry, device: Device) -> None:
|
||||
"""Update device registry with new firmware version."""
|
||||
if (
|
||||
device_entry := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, str(device.serial_number))}
|
||||
)
|
||||
) and device_entry.sw_version != device.firmware_version:
|
||||
device_registry.async_update_device(
|
||||
device_id=device_entry.id, sw_version=device.firmware_version
|
||||
)
|
||||
|
||||
@@ -16,8 +16,9 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import CONNECTED_PLC_DEVICES, CONNECTED_TO_ROUTER
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -18,8 +18,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import DOMAIN, IDENTIFY, PAIRING, RESTART, START_WPS
|
||||
from .coordinator import DevoloHomeNetworkConfigEntry
|
||||
from .entity import DevoloEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -17,8 +17,8 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import DOMAIN, PRODUCT, SERIAL_NUMBER, TITLE
|
||||
from .coordinator import DevoloHomeNetworkConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,44 +1,13 @@
|
||||
"""Base coordinator."""
|
||||
|
||||
from asyncio import Semaphore
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Awaitable, Callable
|
||||
from datetime import timedelta
|
||||
from logging import Logger
|
||||
from typing import Any
|
||||
|
||||
from devolo_plc_api import Device
|
||||
from devolo_plc_api.device_api import (
|
||||
ConnectedStationInfo,
|
||||
NeighborAPInfo,
|
||||
UpdateFirmwareCheck,
|
||||
WifiGuestAccessGet,
|
||||
)
|
||||
from devolo_plc_api.exceptions.device import DevicePasswordProtected, DeviceUnavailable
|
||||
from devolo_plc_api.plcnet_api import LogicalNetwork
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import (
|
||||
CONNECTED_PLC_DEVICES,
|
||||
CONNECTED_WIFI_CLIENTS,
|
||||
DOMAIN,
|
||||
FIRMWARE_UPDATE_INTERVAL,
|
||||
LAST_RESTART,
|
||||
LONG_UPDATE_INTERVAL,
|
||||
NEIGHBORING_WIFI_NETWORKS,
|
||||
REGULAR_FIRMWARE,
|
||||
SHORT_UPDATE_INTERVAL,
|
||||
SWITCH_GUEST_WIFI,
|
||||
SWITCH_LEDS,
|
||||
)
|
||||
|
||||
SEMAPHORE = Semaphore(1)
|
||||
|
||||
type DevoloHomeNetworkConfigEntry = ConfigEntry[DevoloHomeNetworkData]
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
|
||||
class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
@@ -49,256 +18,24 @@ class DevoloDataUpdateCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: DevoloHomeNetworkConfigEntry,
|
||||
config_entry: ConfigEntry,
|
||||
name: str,
|
||||
update_interval: timedelta | None = None,
|
||||
semaphore: Semaphore,
|
||||
update_interval: timedelta,
|
||||
update_method: Callable[[], Awaitable[_DataT]],
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
self.device = config_entry.runtime_data.device
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
update_method=update_method,
|
||||
)
|
||||
self._semaphore = semaphore
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
"""Fetch the latest data from the source."""
|
||||
self.update_sw_version()
|
||||
async with SEMAPHORE:
|
||||
try:
|
||||
return await super()._async_update_data()
|
||||
except DeviceUnavailable as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
except DevicePasswordProtected as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="password_wrong"
|
||||
) from err
|
||||
|
||||
@callback
|
||||
def update_sw_version(self) -> None:
|
||||
"""Update device registry with new firmware version, if it changed at runtime."""
|
||||
device_registry = dr.async_get(self.hass)
|
||||
if (
|
||||
device_entry := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, self.device.serial_number)}
|
||||
)
|
||||
) and device_entry.sw_version != self.device.firmware_version:
|
||||
device_registry.async_update_device(
|
||||
device_id=device_entry.id, sw_version=self.device.firmware_version
|
||||
)
|
||||
|
||||
|
||||
class DevoloFirmwareUpdateCoordinator(DevoloDataUpdateCoordinator[UpdateFirmwareCheck]):
|
||||
"""Class to manage fetching data from the UpdateFirmwareCheck endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = REGULAR_FIRMWARE,
|
||||
update_interval: timedelta | None = FIRMWARE_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_firmware_available
|
||||
|
||||
async def async_update_firmware_available(self) -> UpdateFirmwareCheck:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_check_firmware_available()
|
||||
|
||||
|
||||
class DevoloLedSettingsGetCoordinator(DevoloDataUpdateCoordinator[bool]):
|
||||
"""Class to manage fetching data from the LedSettingsGet endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = SWITCH_LEDS,
|
||||
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_led_status
|
||||
|
||||
async def async_update_led_status(self) -> bool:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_get_led_setting()
|
||||
|
||||
|
||||
class DevoloLogicalNetworkCoordinator(DevoloDataUpdateCoordinator[LogicalNetwork]):
|
||||
"""Class to manage fetching data from the GetNetworkOverview endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = CONNECTED_PLC_DEVICES,
|
||||
update_interval: timedelta | None = LONG_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_connected_plc_devices
|
||||
|
||||
async def async_update_connected_plc_devices(self) -> LogicalNetwork:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.plcnet
|
||||
return await self.device.plcnet.async_get_network_overview()
|
||||
|
||||
|
||||
class DevoloUptimeGetCoordinator(DevoloDataUpdateCoordinator[int]):
|
||||
"""Class to manage fetching data from the UptimeGet endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = LAST_RESTART,
|
||||
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_last_restart
|
||||
|
||||
async def async_update_last_restart(self) -> int:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_uptime()
|
||||
|
||||
|
||||
class DevoloWifiConnectedStationsGetCoordinator(
|
||||
DevoloDataUpdateCoordinator[list[ConnectedStationInfo]]
|
||||
):
|
||||
"""Class to manage fetching data from the WifiGuestAccessGet endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = CONNECTED_WIFI_CLIENTS,
|
||||
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_get_wifi_connected_station
|
||||
|
||||
async def async_get_wifi_connected_station(self) -> list[ConnectedStationInfo]:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_get_wifi_connected_station()
|
||||
|
||||
|
||||
class DevoloWifiGuestAccessGetCoordinator(
|
||||
DevoloDataUpdateCoordinator[WifiGuestAccessGet]
|
||||
):
|
||||
"""Class to manage fetching data from the WifiGuestAccessGet endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = SWITCH_GUEST_WIFI,
|
||||
update_interval: timedelta | None = SHORT_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_guest_wifi_status
|
||||
|
||||
async def async_update_guest_wifi_status(self) -> WifiGuestAccessGet:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_get_wifi_guest_access()
|
||||
|
||||
|
||||
class DevoloWifiNeighborAPsGetCoordinator(
|
||||
DevoloDataUpdateCoordinator[list[NeighborAPInfo]]
|
||||
):
|
||||
"""Class to manage fetching data from the WifiNeighborAPsGet endpoint."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
logger: Logger,
|
||||
*,
|
||||
config_entry: ConfigEntry,
|
||||
name: str = NEIGHBORING_WIFI_NETWORKS,
|
||||
update_interval: timedelta | None = LONG_UPDATE_INTERVAL,
|
||||
) -> None:
|
||||
"""Initialize global data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
config_entry=config_entry,
|
||||
name=name,
|
||||
update_interval=update_interval,
|
||||
)
|
||||
self.update_method = self.async_update_wifi_neighbor_access_points
|
||||
|
||||
async def async_update_wifi_neighbor_access_points(self) -> list[NeighborAPInfo]:
|
||||
"""Fetch data from API endpoint."""
|
||||
assert self.device.device
|
||||
return await self.device.device.async_get_wifi_neighbor_access_points()
|
||||
|
||||
|
||||
@dataclass
|
||||
class DevoloHomeNetworkData:
|
||||
"""The devolo Home Network data."""
|
||||
|
||||
device: Device
|
||||
coordinators: dict[str, DevoloDataUpdateCoordinator[Any]]
|
||||
async with self._semaphore:
|
||||
return await super()._async_update_data()
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import CONNECTED_WIFI_CLIENTS, DOMAIN, WIFI_APTYPE, WIFI_BANDS
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_PASSWORD
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import DevoloHomeNetworkConfigEntry
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
|
||||
TO_REDACT = {CONF_PASSWORD}
|
||||
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, Device
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
|
||||
type _DataType = (
|
||||
LogicalNetwork
|
||||
|
||||
@@ -15,8 +15,9 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import IMAGE_GUEST_WIFI, SWITCH_GUEST_WIFI
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.dt import utcnow
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import (
|
||||
CONNECTED_PLC_DEVICES,
|
||||
CONNECTED_WIFI_CLIENTS,
|
||||
@@ -30,7 +31,7 @@ from .const import (
|
||||
PLC_RX_RATE,
|
||||
PLC_TX_RATE,
|
||||
)
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -16,8 +16,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import DOMAIN, SWITCH_GUEST_WIFI, SWITCH_LEDS
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -21,8 +21,9 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DevoloHomeNetworkConfigEntry
|
||||
from .const import DOMAIN, REGULAR_FIRMWARE
|
||||
from .coordinator import DevoloDataUpdateCoordinator, DevoloHomeNetworkConfigEntry
|
||||
from .coordinator import DevoloDataUpdateCoordinator
|
||||
from .entity import DevoloCoordinatorEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
"""The dlib_face_detect component."""
|
||||
|
||||
DOMAIN = "dlib_face_detect"
|
||||
|
||||
@@ -11,17 +11,10 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCATION, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA
|
||||
|
||||
|
||||
@@ -32,20 +25,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Detect",
|
||||
},
|
||||
)
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
add_entities(
|
||||
DlibFaceDetectEntity(camera[CONF_ENTITY_ID], camera.get(CONF_NAME))
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
"""The dlib_face_identify component."""
|
||||
|
||||
CONF_FACES = "faces"
|
||||
DOMAIN = "dlib_face_identify"
|
||||
|
||||
@@ -15,20 +15,14 @@ from homeassistant.components.image_processing import (
|
||||
ImageProcessingFaceEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_NAME, CONF_ENTITY_ID, CONF_NAME, CONF_SOURCE
|
||||
from homeassistant.core import (
|
||||
DOMAIN as HOMEASSISTANT_DOMAIN,
|
||||
HomeAssistant,
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, split_entity_id
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_FACES, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_FACES = "faces"
|
||||
|
||||
PLATFORM_SCHEMA = IMAGE_PROCESSING_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@@ -45,21 +39,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Dlib Face detection platform."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Dlib Face Identify",
|
||||
},
|
||||
)
|
||||
|
||||
confidence: float = config[CONF_CONFIDENCE]
|
||||
faces: dict[str, str] = config[CONF_FACES]
|
||||
source: list[dict[str, str]] = config[CONF_SOURCE]
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
from typing import Any, Literal
|
||||
from typing import Any
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
@@ -62,16 +62,16 @@ async def async_validate_hostname(
|
||||
"""Validate hostname."""
|
||||
|
||||
async def async_check(
|
||||
hostname: str, resolver: str, qtype: Literal["A", "AAAA"], port: int = 53
|
||||
hostname: str, resolver: str, qtype: str, port: int = 53
|
||||
) -> bool:
|
||||
"""Return if able to resolve hostname."""
|
||||
result: bool = False
|
||||
result = False
|
||||
with contextlib.suppress(DNSError):
|
||||
_resolver = aiodns.DNSResolver(
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
result = bool(
|
||||
await aiodns.DNSResolver( # type: ignore[call-overload]
|
||||
nameservers=[resolver], udp_port=port, tcp_port=port
|
||||
).query(hostname, qtype)
|
||||
)
|
||||
result = bool(await _resolver.query(hostname, qtype))
|
||||
|
||||
return result
|
||||
|
||||
result: dict[str, bool] = {}
|
||||
|
||||
@@ -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.3.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==13.2.1"]
|
||||
}
|
||||
|
||||
@@ -1,6 +1 @@
|
||||
"""The eddystone_temperature component."""
|
||||
|
||||
DOMAIN = "eddystone_temperature"
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
@@ -23,18 +23,17 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, Event, HomeAssistant
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import CONF_BEACONS, CONF_INSTANCE, CONF_NAMESPACE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_BEACONS = "beacons"
|
||||
CONF_BT_DEVICE_ID = "bt_device_id"
|
||||
|
||||
CONF_INSTANCE = "instance"
|
||||
CONF_NAMESPACE = "namespace"
|
||||
|
||||
BEACON_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -59,21 +58,6 @@ def setup_platform(
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Validate configuration, create devices and start monitoring thread."""
|
||||
create_issue(
|
||||
hass,
|
||||
HOMEASSISTANT_DOMAIN,
|
||||
f"deprecated_system_packages_yaml_integration_{DOMAIN}",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_system_packages_yaml_integration",
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"integration_title": "Eddystone",
|
||||
},
|
||||
)
|
||||
|
||||
bt_device_id: int = config[CONF_BT_DEVICE_ID]
|
||||
|
||||
beacons: dict[str, dict[str, str]] = config[CONF_BEACONS]
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.heater import EheimDigitalHeater
|
||||
from eheimdigital.types import HeaterMode, HeaterUnit
|
||||
from eheimdigital.types import EheimDigitalClientError, HeaterMode, HeaterUnit
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
PRESET_NONE,
|
||||
@@ -20,11 +20,12 @@ from homeassistant.const import (
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import HEATER_BIO_MODE, HEATER_PRESET_TO_HEATER_MODE, HEATER_SMART_MODE
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -82,28 +83,34 @@ class EheimDigitalHeaterClimate(EheimDigitalEntity[EheimDigitalHeater], ClimateE
|
||||
self._attr_unique_id = self._device_address
|
||||
self._async_update_attrs()
|
||||
|
||||
@exception_handler
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set the preset mode."""
|
||||
if preset_mode in HEATER_PRESET_TO_HEATER_MODE:
|
||||
await self._device.set_operation_mode(
|
||||
HEATER_PRESET_TO_HEATER_MODE[preset_mode]
|
||||
)
|
||||
try:
|
||||
if preset_mode in HEATER_PRESET_TO_HEATER_MODE:
|
||||
await self._device.set_operation_mode(
|
||||
HEATER_PRESET_TO_HEATER_MODE[preset_mode]
|
||||
)
|
||||
except EheimDigitalClientError as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
@exception_handler
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set a new temperature."""
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE])
|
||||
try:
|
||||
if ATTR_TEMPERATURE in kwargs:
|
||||
await self._device.set_target_temperature(kwargs[ATTR_TEMPERATURE])
|
||||
except EheimDigitalClientError as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
@exception_handler
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the heating mode."""
|
||||
match hvac_mode:
|
||||
case HVACMode.OFF:
|
||||
await self._device.set_active(active=False)
|
||||
case HVACMode.AUTO:
|
||||
await self._device.set_active(active=True)
|
||||
try:
|
||||
match hvac_mode:
|
||||
case HVACMode.OFF:
|
||||
await self._device.set_active(active=False)
|
||||
case HVACMode.AUTO:
|
||||
await self._device.set_active(active=True)
|
||||
except EheimDigitalClientError as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
if self._device.temperature_unit == HeaterUnit.CELSIUS:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Diagnostics for the EHEIM Digital integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry
|
||||
|
||||
TO_REDACT = {"emailAddr", "usrName"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: EheimDigitalConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
return async_redact_data(
|
||||
{"entry": entry.as_dict(), "data": entry.runtime_data.data}, TO_REDACT
|
||||
)
|
||||
@@ -1,15 +1,12 @@
|
||||
"""Base entity for EHEIM Digital."""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import EheimDigitalClientError
|
||||
|
||||
from homeassistant.const import CONF_HOST
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -54,24 +51,3 @@ class EheimDigitalEntity[_DeviceT: EheimDigitalDevice](
|
||||
"""Update attributes when the coordinator updates."""
|
||||
self._async_update_attrs()
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
|
||||
def exception_handler[_EntityT: EheimDigitalEntity[EheimDigitalDevice], **_P](
|
||||
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
|
||||
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
|
||||
"""Decorate AirGradient calls to handle exceptions.
|
||||
|
||||
A decorator that wraps the passed in function, catches AirGradient errors.
|
||||
"""
|
||||
|
||||
async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
|
||||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
except EheimDigitalClientError as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(error)},
|
||||
) from error
|
||||
|
||||
return handler
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
|
||||
from eheimdigital.classic_led_ctrl import EheimDigitalClassicLEDControl
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
from eheimdigital.types import LightMode
|
||||
from eheimdigital.types import EheimDigitalClientError, LightMode
|
||||
|
||||
from homeassistant.components.light import (
|
||||
ATTR_BRIGHTNESS,
|
||||
@@ -15,12 +15,13 @@ from homeassistant.components.light import (
|
||||
LightEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.color import brightness_to_value, value_to_brightness
|
||||
|
||||
from .const import EFFECT_DAYCL_MODE, EFFECT_TO_LIGHT_MODE
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
BRIGHTNESS_SCALE = (1, 100)
|
||||
|
||||
@@ -87,22 +88,30 @@ class EheimDigitalClassicLEDControlLight(
|
||||
"""Return whether the entity is available."""
|
||||
return super().available and self._device.light_level[self._channel] is not None
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
if ATTR_EFFECT in kwargs:
|
||||
await self._device.set_light_mode(EFFECT_TO_LIGHT_MODE[kwargs[ATTR_EFFECT]])
|
||||
return
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
await self._device.turn_on(
|
||||
int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])),
|
||||
self._channel,
|
||||
)
|
||||
if self._device.light_mode == LightMode.DAYCL_MODE:
|
||||
await self._device.set_light_mode(LightMode.MAN_MODE)
|
||||
try:
|
||||
await self._device.turn_on(
|
||||
int(brightness_to_value(BRIGHTNESS_SCALE, kwargs[ATTR_BRIGHTNESS])),
|
||||
self._channel,
|
||||
)
|
||||
except EheimDigitalClientError as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
await self._device.turn_off(self._channel)
|
||||
if self._device.light_mode == LightMode.DAYCL_MODE:
|
||||
await self._device.set_light_mode(LightMode.MAN_MODE)
|
||||
try:
|
||||
await self._device.turn_off(self._channel)
|
||||
except EheimDigitalClientError as err:
|
||||
raise HomeAssistantError from err
|
||||
|
||||
def _async_update_attrs(self) -> None:
|
||||
light_level = self._device.light_level[self._channel]
|
||||
|
||||
@@ -26,7 +26,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -182,7 +182,6 @@ class EheimDigitalNumber(
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
return await self.entity_description.set_value_fn(self._device, value)
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
diagnostics: todo
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: todo
|
||||
@@ -58,7 +58,7 @@ rules:
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -94,7 +94,6 @@ class EheimDigitalSelect(
|
||||
self._attr_unique_id = f"{self._device_address}_{description.key}"
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
return await self.entity_description.set_value_fn(self._device, option)
|
||||
|
||||
|
||||
@@ -101,10 +101,5 @@
|
||||
"name": "Night start time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the EHEIM Digital hub: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -58,12 +58,10 @@ class EheimDigitalClassicVarioSwitch(
|
||||
self._async_update_attrs()
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
await self._device.set_active(active=False)
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
await self._device.set_active(active=True)
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
|
||||
from .entity import EheimDigitalEntity, exception_handler
|
||||
from .entity import EheimDigitalEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -122,7 +122,6 @@ class EheimDigitalTime(
|
||||
self._attr_unique_id = f"{device.mac_address}_{description.key}"
|
||||
|
||||
@override
|
||||
@exception_handler
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the time."""
|
||||
return await self.entity_description.set_value_fn(self._device, value)
|
||||
|
||||
@@ -180,15 +180,9 @@ class EnphaseUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
return
|
||||
|
||||
device_registry.async_get_or_create(
|
||||
config_entry_id=self.config_entry.entry_id,
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
self.envoy_serial_number,
|
||||
)
|
||||
},
|
||||
connections={connection},
|
||||
device_registry.async_update_device(
|
||||
device_id=envoy_device.id,
|
||||
new_connections={connection},
|
||||
)
|
||||
_LOGGER.debug("added connection: %s to %s", connection, self.name)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["env_canada"],
|
||||
"requirements": ["env-canada==0.11.2"]
|
||||
"requirements": ["env-canada==0.10.2"]
|
||||
}
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["eq3btsmart"],
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.16.0"]
|
||||
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
|
||||
}
|
||||
|
||||
@@ -226,7 +226,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
|
||||
_static_info: _InfoT
|
||||
_state: _StateT
|
||||
_has_state: bool
|
||||
unique_id: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||
|
||||
from aioesphomeapi import (
|
||||
@@ -22,7 +23,6 @@ from aioesphomeapi import (
|
||||
RequiresEncryptionAPIError,
|
||||
UserService,
|
||||
UserServiceArgType,
|
||||
parse_log_message,
|
||||
)
|
||||
from awesomeversion import AwesomeVersion
|
||||
import voluptuous as vol
|
||||
@@ -110,6 +110,11 @@ LOGGER_TO_LOG_LEVEL = {
|
||||
logging.ERROR: LogLevel.LOG_LEVEL_ERROR,
|
||||
logging.CRITICAL: LogLevel.LOG_LEVEL_ERROR,
|
||||
}
|
||||
# 7-bit and 8-bit C1 ANSI sequences
|
||||
# https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python
|
||||
ANSI_ESCAPE_78BIT = re.compile(
|
||||
rb"(?:\x1B[@-Z\\-_]|[\x80-\x9A\x9C-\x9F]|(?:\x1B\[|\x9B)[0-?]*[ -/]*[@-~])"
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
@@ -382,15 +387,13 @@ class ESPHomeManager:
|
||||
|
||||
def _async_on_log(self, msg: SubscribeLogsResponse) -> None:
|
||||
"""Handle a log message from the API."""
|
||||
for line in parse_log_message(
|
||||
msg.message.decode("utf-8", "backslashreplace"), "", strip_ansi_escapes=True
|
||||
):
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
line,
|
||||
)
|
||||
log: bytes = msg.message
|
||||
_LOGGER.log(
|
||||
LOG_LEVEL_TO_LOGGER.get(msg.level, logging.DEBUG),
|
||||
"%s: %s",
|
||||
self.entry.title,
|
||||
ANSI_ESCAPE_78BIT.sub(b"", log).decode("utf-8", "backslashreplace"),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_get_equivalent_log_level(self) -> LogLevel:
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.1",
|
||||
"aioesphomeapi==31.1.0",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
"bleak-esphome==2.15.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ class EsphomeMediaPlayer(
|
||||
if self._static_info.supports_pause:
|
||||
flags |= MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY
|
||||
self._attr_supported_features = flags
|
||||
self._entry_data.media_player_formats[self.unique_id] = cast(
|
||||
self._entry_data.media_player_formats[static_info.unique_id] = cast(
|
||||
MediaPlayerInfo, static_info
|
||||
).supported_formats
|
||||
|
||||
@@ -114,8 +114,9 @@ class EsphomeMediaPlayer(
|
||||
media_id = async_process_play_media_url(self.hass, media_id)
|
||||
announcement = kwargs.get(ATTR_MEDIA_ANNOUNCE)
|
||||
bypass_proxy = kwargs.get(ATTR_MEDIA_EXTRA, {}).get(ATTR_BYPASS_PROXY)
|
||||
|
||||
supported_formats: list[MediaPlayerSupportedFormat] | None = (
|
||||
self._entry_data.media_player_formats.get(self.unique_id)
|
||||
self._entry_data.media_player_formats.get(self._static_info.unique_id)
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -138,7 +139,7 @@ class EsphomeMediaPlayer(
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Handle entity being removed."""
|
||||
await super().async_will_remove_from_hass()
|
||||
self._entry_data.media_player_formats.pop(self.unique_id, None)
|
||||
self._entry_data.media_player_formats.pop(self.entity_id, None)
|
||||
|
||||
def _get_proxy_url(
|
||||
self,
|
||||
|
||||
@@ -105,18 +105,9 @@ DATA_SCHEMA_SETUP = vol.Schema(
|
||||
)
|
||||
|
||||
BASE_OPTIONS_SCHEMA = {
|
||||
vol.Optional(CONF_ENTITY_ID): EntitySelector(EntitySelectorConfig(read_only=True)),
|
||||
vol.Optional(CONF_FILTER_NAME): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=FILTERS,
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key=CONF_FILTER_NAME,
|
||||
read_only=True,
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_FILTER_PRECISION, default=DEFAULT_PRECISION): NumberSelector(
|
||||
NumberSelectorConfig(min=0, step=1, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
OUTLIER_SCHEMA = vol.Schema(
|
||||
|
||||
@@ -23,16 +23,12 @@
|
||||
"data": {
|
||||
"window_size": "Window size",
|
||||
"precision": "Precision",
|
||||
"radius": "Radius",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"radius": "Radius"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "Size of the window of previous states.",
|
||||
"precision": "Defines the number of decimal places of the calculated sensor value.",
|
||||
"radius": "Band radius from median of previous states.",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"radius": "Band radius from median of previous states."
|
||||
}
|
||||
},
|
||||
"lowpass": {
|
||||
@@ -40,16 +36,12 @@
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"time_constant": "Time constant",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"time_constant": "Time constant"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"time_constant": "Loosely relates to the amount of time it takes for a state to influence the output.",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"time_constant": "Loosely relates to the amount of time it takes for a state to influence the output."
|
||||
}
|
||||
},
|
||||
"range": {
|
||||
@@ -57,16 +49,12 @@
|
||||
"data": {
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"lower_bound": "Lower bound",
|
||||
"upper_bound": "Upper bound",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"upper_bound": "Upper bound"
|
||||
},
|
||||
"data_description": {
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"lower_bound": "Lower bound for filter range.",
|
||||
"upper_bound": "Upper bound for filter range.",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"upper_bound": "Upper bound for filter range."
|
||||
}
|
||||
},
|
||||
"time_simple_moving_average": {
|
||||
@@ -74,46 +62,34 @@
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"type": "Type",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"type": "Type"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"type": "Defines the type of Simple Moving Average.",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"type": "Defines the type of Simple Moving Average."
|
||||
}
|
||||
},
|
||||
"throttle": {
|
||||
"description": "[%key:component::filter::config::step::outlier::description%]",
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
|
||||
}
|
||||
},
|
||||
"time_throttle": {
|
||||
"description": "[%key:component::filter::config::step::outlier::description%]",
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -128,16 +104,12 @@
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"radius": "[%key:component::filter::config::step::outlier::data::radius%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"radius": "[%key:component::filter::config::step::outlier::data::radius%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"radius": "[%key:component::filter::config::step::outlier::data_description::radius%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"radius": "[%key:component::filter::config::step::outlier::data_description::radius%]"
|
||||
}
|
||||
},
|
||||
"lowpass": {
|
||||
@@ -145,16 +117,12 @@
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"time_constant": "[%key:component::filter::config::step::lowpass::data::time_constant%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"time_constant": "[%key:component::filter::config::step::lowpass::data_description::time_constant%]"
|
||||
}
|
||||
},
|
||||
"range": {
|
||||
@@ -162,16 +130,12 @@
|
||||
"data": {
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"lower_bound": "[%key:component::filter::config::step::range::data::lower_bound%]",
|
||||
"upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"upper_bound": "[%key:component::filter::config::step::range::data::upper_bound%]"
|
||||
},
|
||||
"data_description": {
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"lower_bound": "[%key:component::filter::config::step::range::data_description::lower_bound%]",
|
||||
"upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"upper_bound": "[%key:component::filter::config::step::range::data_description::upper_bound%]"
|
||||
}
|
||||
},
|
||||
"time_simple_moving_average": {
|
||||
@@ -179,46 +143,34 @@
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"type": "[%key:component::filter::config::step::time_simple_moving_average::data::type%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"type": "[%key:component::filter::config::step::time_simple_moving_average::data_description::type%]"
|
||||
}
|
||||
},
|
||||
"throttle": {
|
||||
"description": "[%key:component::filter::config::step::outlier::description%]",
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
|
||||
}
|
||||
},
|
||||
"time_throttle": {
|
||||
"description": "[%key:component::filter::config::step::outlier::description%]",
|
||||
"data": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data::precision%]"
|
||||
},
|
||||
"data_description": {
|
||||
"window_size": "[%key:component::filter::config::step::outlier::data_description::window_size%]",
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]",
|
||||
"entity_id": "[%key:component::filter::config::step::user::data_description::entity_id%]",
|
||||
"filter": "[%key:component::filter::config::step::user::data_description::filter%]"
|
||||
"precision": "[%key:component::filter::config::step::outlier::data_description::precision%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -29,7 +28,6 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
key="rate_down",
|
||||
name="Freebox download speed",
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
|
||||
icon="mdi:download-network",
|
||||
),
|
||||
@@ -37,7 +35,6 @@ CONNECTION_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
key="rate_up",
|
||||
name="Freebox upload speed",
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBYTES_PER_SECOND,
|
||||
icon="mdi:upload-network",
|
||||
),
|
||||
@@ -84,7 +81,6 @@ async def async_setup_entry(
|
||||
name=f"Freebox {sensor_name}",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
for sensor_name in router.sensors_temperature
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyfronius"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["PyFronius==0.8.0"]
|
||||
"requirements": ["PyFronius==0.7.7"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250531.2"]
|
||||
"requirements": ["home-assistant-frontend==20250516.0"]
|
||||
}
|
||||
|
||||
@@ -84,10 +84,7 @@ async def async_migrate_entry(
|
||||
new[CONF_EXPIRATION] = credentials.expiration.isoformat()
|
||||
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry,
|
||||
data=new,
|
||||
minor_version=2,
|
||||
version=1,
|
||||
config_entry, data=new, minor_version=2, version=1
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
|
||||
@@ -2,20 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from fyta_cli.fyta_models import Plant
|
||||
|
||||
from homeassistant.components.image import (
|
||||
Image,
|
||||
ImageEntity,
|
||||
ImageEntityDescription,
|
||||
valid_image_content_type,
|
||||
)
|
||||
from homeassistant.components.image import ImageEntity, ImageEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -23,30 +12,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .coordinator import FytaConfigEntry, FytaCoordinator
|
||||
from .entity import FytaPlantEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FytaImageEntityDescription(ImageEntityDescription):
|
||||
"""Describes Fyta image entity."""
|
||||
|
||||
url_fn: Callable[[Plant], str]
|
||||
name_key: str | None = None
|
||||
|
||||
|
||||
IMAGES: Final[list[FytaImageEntityDescription]] = [
|
||||
FytaImageEntityDescription(
|
||||
key="plant_image",
|
||||
translation_key="plant_image",
|
||||
url_fn=lambda plant: plant.plant_origin_path,
|
||||
),
|
||||
FytaImageEntityDescription(
|
||||
key="plant_image_user",
|
||||
translation_key="plant_image_user",
|
||||
url_fn=lambda plant: plant.user_picture_path,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -56,17 +21,17 @@ async def async_setup_entry(
|
||||
"""Set up the FYTA plant images."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
description = ImageEntityDescription(key="plant_image")
|
||||
|
||||
async_add_entities(
|
||||
FytaPlantImageEntity(coordinator, entry, description, plant_id)
|
||||
for plant_id in coordinator.fyta.plant_list
|
||||
if plant_id in coordinator.data
|
||||
for description in IMAGES
|
||||
)
|
||||
|
||||
def _async_add_new_device(plant_id: int) -> None:
|
||||
async_add_entities(
|
||||
FytaPlantImageEntity(coordinator, entry, description, plant_id)
|
||||
for description in IMAGES
|
||||
[FytaPlantImageEntity(coordinator, entry, description, plant_id)]
|
||||
)
|
||||
|
||||
coordinator.new_device_callbacks.append(_async_add_new_device)
|
||||
@@ -75,49 +40,26 @@ async def async_setup_entry(
|
||||
class FytaPlantImageEntity(FytaPlantEntity, ImageEntity):
|
||||
"""Represents a Fyta image."""
|
||||
|
||||
entity_description: FytaImageEntityDescription
|
||||
entity_description: ImageEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: FytaCoordinator,
|
||||
entry: ConfigEntry,
|
||||
description: FytaImageEntityDescription,
|
||||
description: ImageEntityDescription,
|
||||
plant_id: int,
|
||||
) -> None:
|
||||
"""Initialize Fyta Image entity."""
|
||||
"""Initiatlize Fyta Image entity."""
|
||||
super().__init__(coordinator, entry, description, plant_id)
|
||||
ImageEntity.__init__(self, coordinator.hass)
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
"""Return bytes of image."""
|
||||
if self.entity_description.key == "plant_image_user":
|
||||
if self._cached_image is None:
|
||||
response = await self.coordinator.fyta.get_plant_image(
|
||||
self.plant.user_picture_path
|
||||
)
|
||||
_LOGGER.debug("Response of downloading user image: %s", response)
|
||||
if response is None:
|
||||
_LOGGER.debug(
|
||||
"%s: Error getting new image from %s",
|
||||
self.entity_id,
|
||||
self.plant.user_picture_path,
|
||||
)
|
||||
return None
|
||||
|
||||
content_type, raw_image = response
|
||||
self._cached_image = Image(
|
||||
valid_image_content_type(content_type), raw_image
|
||||
)
|
||||
|
||||
return self._cached_image.content
|
||||
return await ImageEntity.async_image(self)
|
||||
self._attr_name = None
|
||||
|
||||
@property
|
||||
def image_url(self) -> str:
|
||||
"""Return the image_url for this plant."""
|
||||
url = self.entity_description.url_fn(self.plant)
|
||||
|
||||
if url != self._attr_image_url:
|
||||
self._cached_image = None
|
||||
"""Return the image_url for this sensor."""
|
||||
image = self.plant.plant_origin_path
|
||||
if image != self._attr_image_url:
|
||||
self._attr_image_last_updated = datetime.now()
|
||||
return url
|
||||
|
||||
return image
|
||||
|
||||
@@ -61,14 +61,6 @@
|
||||
"name": "Sensor update available"
|
||||
}
|
||||
},
|
||||
"image": {
|
||||
"plant_image": {
|
||||
"name": "Plant image"
|
||||
},
|
||||
"plant_image_user": {
|
||||
"name": "User image"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"scientific_name": {
|
||||
"name": "Scientific name"
|
||||
|
||||
@@ -5,18 +5,11 @@ import voluptuous as vol
|
||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_NAME, CONF_UNIQUE_ID, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv, discovery
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "generic_hygrostat"
|
||||
@@ -95,54 +88,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.options[CONF_HUMIDIFIER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||
# but not the humidity sensor because the generic_hygrostat adds itself to the
|
||||
# humidifier's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HUMIDIFIER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HUMIDIFIER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
"""The generic_thermostat component."""
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_entity_id_to_device_id,
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_entity_registry_updated_event
|
||||
from homeassistant.helpers.helper_integration import async_handle_source_entity_changes
|
||||
|
||||
from .const import CONF_HEATER, CONF_SENSOR, PLATFORMS
|
||||
from .const import CONF_HEATER, PLATFORMS
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
@@ -21,55 +17,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.entry_id,
|
||||
entry.options[CONF_HEATER],
|
||||
)
|
||||
|
||||
def set_humidifier_entity_id_or_uuid(source_entity_id: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_HEATER: source_entity_id},
|
||||
)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we need to clean the device links.
|
||||
async_remove_stale_devices_links_keep_entity_device(hass, entry.entry_id, None)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||
# not the temperature sensor because the generic_hygrostat adds itself to the
|
||||
# heater's device.
|
||||
async_handle_source_entity_changes(
|
||||
hass,
|
||||
helper_config_entry_id=entry.entry_id,
|
||||
set_source_entity_id_or_uuid=set_humidifier_entity_id_or_uuid,
|
||||
source_device_id=async_entity_id_to_device_id(
|
||||
hass, entry.options[CONF_HEATER]
|
||||
),
|
||||
source_entity_id_or_uuid=entry.options[CONF_HEATER],
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
|
||||
async def async_sensor_updated(
|
||||
event: Event[er.EventEntityRegistryUpdatedData],
|
||||
) -> None:
|
||||
"""Handle entity registry update."""
|
||||
data = event.data
|
||||
if data["action"] != "update":
|
||||
return
|
||||
if "entity_id" not in data["changes"]:
|
||||
return
|
||||
|
||||
# Entity_id changed, update the config entry
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
hass, entry.options[CONF_SENSOR], async_sensor_updated
|
||||
)
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["go2rtc-client==0.2.1"],
|
||||
"requirements": ["go2rtc-client==0.1.3b0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import aiohttp
|
||||
from aiohttp import web
|
||||
from gassist_text import TextAssistant
|
||||
from google.oauth2.credentials import Credentials
|
||||
from grpc import RpcError
|
||||
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.media_player import (
|
||||
@@ -26,7 +25,6 @@ from homeassistant.components.media_player import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_ENTITY_ID, CONF_ACCESS_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
@@ -85,17 +83,7 @@ async def async_send_text_commands(
|
||||
) as assistant:
|
||||
command_response_list = []
|
||||
for command in commands:
|
||||
try:
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
except RpcError as err:
|
||||
_LOGGER.error(
|
||||
"Failed to send command '%s' to Google Assistant: %s",
|
||||
command,
|
||||
err,
|
||||
)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="grpc_error"
|
||||
) from err
|
||||
resp = await hass.async_add_executor_job(assistant.assist, command)
|
||||
text_response = resp[0]
|
||||
_LOGGER.debug("command: %s\nresponse: %s", command, text_response)
|
||||
audio_response = resp[2]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user