mirror of
https://github.com/home-assistant/core.git
synced 2026-03-07 14:34:56 +01:00
Compare commits
114 Commits
cover_remo
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f552b8221f | ||
|
|
55dc5392f9 | ||
|
|
5b93aeae38 | ||
|
|
33610bb1a1 | ||
|
|
6c3cebe413 | ||
|
|
5346895d9b | ||
|
|
05c3f08c6c | ||
|
|
1ce025733d | ||
|
|
1537ea86b8 | ||
|
|
ec137870fa | ||
|
|
816ee7f53e | ||
|
|
6e7eeec827 | ||
|
|
d100477a22 | ||
|
|
98ac6dd2c1 | ||
|
|
6b30969f60 | ||
|
|
e9a6b5d662 | ||
|
|
f95f3f9982 | ||
|
|
3f884a8cd1 | ||
|
|
10f284932e | ||
|
|
e1c4e6dc42 | ||
|
|
0976e7de4e | ||
|
|
ae1012b2f0 | ||
|
|
bb7c4faca5 | ||
|
|
0b1be61336 | ||
|
|
3ec44024a2 | ||
|
|
1200cc5779 | ||
|
|
d632931f74 | ||
|
|
2f9faa53a1 | ||
|
|
718607a758 | ||
|
|
3789156559 | ||
|
|
042ce6f2de | ||
|
|
0a5908002f | ||
|
|
3a5f71e10a | ||
|
|
04e4b05ab0 | ||
|
|
c2c5232899 | ||
|
|
593610094e | ||
|
|
47cb7870ea | ||
|
|
045b626e24 | ||
|
|
bea5468dee | ||
|
|
04fc12cc26 | ||
|
|
fec33ad42b | ||
|
|
07e323f1e9 | ||
|
|
ebe2612713 | ||
|
|
88ca668562 | ||
|
|
1d46ac0b64 | ||
|
|
13a5e6e85f | ||
|
|
d2665f03ff | ||
|
|
80412e4973 | ||
|
|
818d9f774e | ||
|
|
012e78d625 | ||
|
|
74abedbcd2 | ||
|
|
e16fb6b5a5 | ||
|
|
8906e5dcb5 | ||
|
|
10067c208a | ||
|
|
d4143205e9 | ||
|
|
a4da363ff2 | ||
|
|
bc9ae3dad6 | ||
|
|
9e5daaa784 | ||
|
|
ff0a6757cd | ||
|
|
62ffeeccb0 | ||
|
|
1afe00670e | ||
|
|
500ffe8153 | ||
|
|
2cebb28a1b | ||
|
|
80bfba0981 | ||
|
|
882e499375 | ||
|
|
e89aafc8e2 | ||
|
|
66ae5ab543 | ||
|
|
75d39c0b02 | ||
|
|
989133cb16 | ||
|
|
f559f8e014 | ||
|
|
a95207f2ef | ||
|
|
2c28a93ea0 | ||
|
|
3ff97a0820 | ||
|
|
f7a56447ae | ||
|
|
dfd086f253 | ||
|
|
b6a166ce48 | ||
|
|
e93b724ce4 | ||
|
|
d0b25ccc01 | ||
|
|
0a3ef64f28 | ||
|
|
e9ce3ffff9 | ||
|
|
55415b1559 | ||
|
|
0160dbf3a6 | ||
|
|
7dd83b1e8f | ||
|
|
e502f5f249 | ||
|
|
6e93ebc912 | ||
|
|
9a4fdf7f80 | ||
|
|
76d69a5f53 | ||
|
|
ae40c0cf4b | ||
|
|
078647d128 | ||
|
|
8a637c4e5b | ||
|
|
9e9daff26d | ||
|
|
41aeedaa82 | ||
|
|
a8297ae65d | ||
|
|
b7f1171c08 | ||
|
|
226f606cb9 | ||
|
|
9472be39f2 | ||
|
|
67a9e42b19 | ||
|
|
ba1837859f | ||
|
|
4a301eceac | ||
|
|
d138a99e62 | ||
|
|
a431f84dc9 | ||
|
|
aa9534600e | ||
|
|
54fa49e754 | ||
|
|
459b6152f4 | ||
|
|
60c8d997ca | ||
|
|
a598368895 | ||
|
|
2ff1499c48 | ||
|
|
348ddbe124 | ||
|
|
71ed43faf2 | ||
|
|
dc69a90296 | ||
|
|
f5db8e6ba4 | ||
|
|
b82a26ef68 | ||
|
|
0eaaeedf11 | ||
|
|
62e26e53ac |
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -209,4 +209,4 @@ jobs:
|
||||
skip-binary: aiohttp;charset-normalizer;grpcio;multidict;SQLAlchemy;propcache;protobuf;pymicro-vad;yarl
|
||||
constraints: "homeassistant/package_constraints.txt"
|
||||
requirements-diff: "requirements_diff.txt"
|
||||
requirements: "requirements_all.txt"
|
||||
requirements: "requirements_all_wheels_${{ matrix.arch }}.txt"
|
||||
|
||||
@@ -70,7 +70,7 @@ from .const import (
|
||||
SIGNAL_BOOTSTRAP_INTEGRATIONS,
|
||||
)
|
||||
from .core_config import async_process_ha_core_config
|
||||
from .exceptions import HomeAssistantError
|
||||
from .exceptions import HomeAssistantError, UnsupportedStorageVersionError
|
||||
from .helpers import (
|
||||
area_registry,
|
||||
category_registry,
|
||||
@@ -239,6 +239,8 @@ DEFAULT_INTEGRATIONS = {
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
# These integrations are set up if recovery mode is activated.
|
||||
"backup",
|
||||
"cloud",
|
||||
"frontend",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||
@@ -433,32 +435,56 @@ def _init_blocking_io_modules_in_executor() -> None:
|
||||
is_docker_env()
|
||||
|
||||
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
|
||||
"""Load the registries and modules that will do blocking I/O."""
|
||||
async def async_load_base_functionality(hass: core.HomeAssistant) -> bool:
|
||||
"""Load the registries and modules that will do blocking I/O.
|
||||
|
||||
Return whether loading succeeded.
|
||||
"""
|
||||
if DATA_REGISTRIES_LOADED in hass.data:
|
||||
return
|
||||
return True
|
||||
|
||||
hass.data[DATA_REGISTRIES_LOADED] = None
|
||||
entity.async_setup(hass)
|
||||
frame.async_setup(hass)
|
||||
template.async_setup(hass)
|
||||
translation.async_setup(hass)
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass)),
|
||||
create_eager_task(category_registry.async_load(hass)),
|
||||
create_eager_task(device_registry.async_load(hass)),
|
||||
create_eager_task(entity_registry.async_load(hass)),
|
||||
create_eager_task(floor_registry.async_load(hass)),
|
||||
create_eager_task(issue_registry.async_load(hass)),
|
||||
create_eager_task(label_registry.async_load(hass)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
|
||||
recovery = hass.config.recovery_mode
|
||||
try:
|
||||
await asyncio.gather(
|
||||
create_eager_task(get_internal_store_manager(hass).async_initialize()),
|
||||
create_eager_task(area_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(category_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(device_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(entity_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(floor_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(issue_registry.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(label_registry.async_load(hass, load_empty=recovery)),
|
||||
hass.async_add_executor_job(_init_blocking_io_modules_in_executor),
|
||||
create_eager_task(template.async_load_custom_templates(hass)),
|
||||
create_eager_task(restore_state.async_load(hass, load_empty=recovery)),
|
||||
create_eager_task(hass.config_entries.async_initialize()),
|
||||
create_eager_task(async_get_system_info(hass)),
|
||||
create_eager_task(condition.async_setup(hass)),
|
||||
create_eager_task(trigger.async_setup(hass)),
|
||||
)
|
||||
except UnsupportedStorageVersionError as err:
|
||||
# If we're already in recovery mode, we don't want to handle the exception
|
||||
# and activate recovery mode again, as that would lead to an infinite loop.
|
||||
if recovery:
|
||||
raise
|
||||
|
||||
_LOGGER.error(
|
||||
"Storage file %s was created by a newer version of Home Assistant"
|
||||
" (storage version %s > %s); activating recovery mode; on-disk data"
|
||||
" is preserved; upgrade Home Assistant or restore from a backup",
|
||||
err.storage_key,
|
||||
err.found_version,
|
||||
err.max_supported_version,
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_from_config_dict(
|
||||
@@ -475,7 +501,9 @@ async def async_from_config_dict(
|
||||
# Prime custom component cache early so we know if registry entries are tied
|
||||
# to a custom integration
|
||||
await loader.async_get_custom_components(hass)
|
||||
await async_load_base_functionality(hass)
|
||||
|
||||
if not await async_load_base_functionality(hass):
|
||||
return None
|
||||
|
||||
# Set up core.
|
||||
_LOGGER.debug("Setting up %s", CORE_INTEGRATIONS)
|
||||
|
||||
5
homeassistant/brands/ubisys.json
Normal file
5
homeassistant/brands/ubisys.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"domain": "ubisys",
|
||||
"name": "Ubisys",
|
||||
"iot_standards": ["zigbee"]
|
||||
}
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["accuweather"],
|
||||
"requirements": ["accuweather==5.0.0"]
|
||||
"requirements": ["accuweather==5.1.0"]
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
|
||||
)
|
||||
|
||||
return {
|
||||
"can_reach_server": system_health.async_check_can_reach_url(hass, ENDPOINT),
|
||||
"can_reach_server": system_health.async_check_can_reach_url(
|
||||
hass, str(ENDPOINT)
|
||||
),
|
||||
"remaining_requests": remaining_requests,
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ class AccuWeatherEntity(
|
||||
{
|
||||
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
|
||||
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
|
||||
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
|
||||
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"),
|
||||
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
|
||||
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][
|
||||
|
||||
@@ -93,7 +93,6 @@ class AirobotNumber(AirobotEntity, NumberEntity):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="set_value_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
else:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"message": "Failed to set temperature to {temperature}."
|
||||
},
|
||||
"set_value_failed": {
|
||||
"message": "Failed to set value: {error}"
|
||||
"message": "Failed to set value."
|
||||
},
|
||||
"switch_turn_off_failed": {
|
||||
"message": "Failed to turn off {switch}."
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,19 +25,20 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""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")
|
||||
model = self.device.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"),
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
self.device.software_version
|
||||
if model != SPEAKER_GROUP_DEVICE_TYPE
|
||||
else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
|
||||
)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{serial_num}-{description.key}"
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==12.0.0"]
|
||||
"requirements": ["aioamazondevices==13.0.0"]
|
||||
}
|
||||
|
||||
@@ -858,6 +858,11 @@ class AnthropicBaseLLMEntity(Entity):
|
||||
]
|
||||
)
|
||||
messages.extend(new_messages)
|
||||
except anthropic.AuthenticationError as err:
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
raise HomeAssistantError(
|
||||
"Authentication error with Anthropic API, reauthentication required"
|
||||
) from err
|
||||
except anthropic.AnthropicError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Sorry, I had a problem talking to Anthropic: {err}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "anthropic",
|
||||
"name": "Anthropic Conversation",
|
||||
"name": "Anthropic",
|
||||
"after_dependencies": ["assist_pipeline", "intent"],
|
||||
"codeowners": ["@Shulyaka"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -30,5 +30,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
|
||||
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
|
||||
}
|
||||
|
||||
@@ -29,12 +29,17 @@ class StoredBackupData(TypedDict):
|
||||
class _BackupStore(Store[StoredBackupData]):
|
||||
"""Class to help storing backup data."""
|
||||
|
||||
# Maximum version we support reading for forward compatibility.
|
||||
# This allows reading data written by a newer HA version after downgrade.
|
||||
_MAX_READABLE_VERSION = 2
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize storage class."""
|
||||
super().__init__(
|
||||
hass,
|
||||
STORAGE_VERSION,
|
||||
STORAGE_KEY,
|
||||
max_readable_version=self._MAX_READABLE_VERSION,
|
||||
minor_version=STORAGE_VERSION_MINOR,
|
||||
)
|
||||
|
||||
@@ -86,8 +91,8 @@ class _BackupStore(Store[StoredBackupData]):
|
||||
# data["config"]["schedule"]["state"] will be removed. The bump to 2 is
|
||||
# planned to happen after a 6 month quiet period with no minor version
|
||||
# changes.
|
||||
# Reject if major version is higher than 2.
|
||||
if old_major_version > 2:
|
||||
# Reject if major version is higher than _MAX_READABLE_VERSION.
|
||||
if old_major_version > self._MAX_READABLE_VERSION:
|
||||
raise NotImplementedError
|
||||
return data
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@
|
||||
"title": "The backup location {agent_id} is unavailable"
|
||||
},
|
||||
"automatic_backup_failed_addons": {
|
||||
"description": "Add-ons {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
|
||||
"title": "Not all add-ons could be included in automatic backup"
|
||||
"description": "Apps {failed_addons} could not be included in automatic backup. Please check the Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
|
||||
"title": "Not all apps could be included in automatic backup"
|
||||
},
|
||||
"automatic_backup_failed_agents_addons_folders": {
|
||||
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Add-ons which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
|
||||
"description": "The automatic backup was created with errors:\n* Locations which the backup could not be uploaded to: {failed_agents}\n* Apps which could not be backed up: {failed_addons}\n* Folders which could not be backed up: {failed_folders}\n\nPlease check the Core and Supervisor logs for more information. Another attempt will be made at the next scheduled time if a backup schedule is configured.",
|
||||
"title": "Automatic backup was created with errors"
|
||||
},
|
||||
"automatic_backup_failed_create": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.0"],
|
||||
"requirements": ["python-bsblan==5.1.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -64,6 +64,8 @@ SENSOR_TYPES: tuple[BSBLanSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
suggested_display_precision=0,
|
||||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data: (
|
||||
data.sensor.total_energy.value
|
||||
if data.sensor.total_energy is not None
|
||||
|
||||
@@ -804,9 +804,24 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
# The lovelace app loops media to prevent timing out, don't show that
|
||||
if (chromecast := self._chromecast) is None or (
|
||||
cast_status := self.cast_status
|
||||
) is None:
|
||||
# Not connected to any chromecast, or not yet got any status
|
||||
return None
|
||||
|
||||
if (
|
||||
chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST
|
||||
and not chromecast.ignore_cec
|
||||
and cast_status.is_active_input is False
|
||||
):
|
||||
# The display interface for the device has been turned off or switched away
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
if self.app_id == CAST_APP_ID_HOMEASSISTANT_LOVELACE:
|
||||
# The lovelace app loops media to prevent timing out, don't show that
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
if (media_status := self._media_status()[0]) is not None:
|
||||
if media_status.player_state == MEDIA_PLAYER_STATE_PLAYING:
|
||||
return MediaPlayerState.PLAYING
|
||||
@@ -817,20 +832,16 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
if media_status.player_is_idle:
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
if self._chromecast is not None and self._chromecast.is_idle:
|
||||
# If library consider us idle, that is our off state
|
||||
# it takes HDMI status into account for cast devices.
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
if self.app_id in APP_IDS_UNRELIABLE_MEDIA_INFO:
|
||||
# Some apps don't report media status, show the player as playing
|
||||
return MediaPlayerState.PLAYING
|
||||
|
||||
if self.app_id is not None:
|
||||
# We have an active app
|
||||
return MediaPlayerState.IDLE
|
||||
if self.app_id in (pychromecast.IDLE_APP_ID, None):
|
||||
# We have no active app or the home screen app. This is
|
||||
# same app as APP_BACKDROP.
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
return None
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@property
|
||||
def media_content_id(self) -> str | None:
|
||||
|
||||
@@ -324,8 +324,8 @@
|
||||
"nano_nr_3": "Nano 3",
|
||||
"nano_nr_4": "Nano 4",
|
||||
"nano_nr_5": "Nano 5",
|
||||
"off": "Off",
|
||||
"on": "On",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"summer": "Summer",
|
||||
"winter": "Winter"
|
||||
}
|
||||
@@ -363,8 +363,8 @@
|
||||
"pump_status": {
|
||||
"name": "Pump status",
|
||||
"state": {
|
||||
"off": "Off",
|
||||
"on": "On"
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]"
|
||||
}
|
||||
},
|
||||
"return_circuit_temperature": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.2.13"]
|
||||
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.3"]
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
|
||||
try:
|
||||
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
|
||||
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
|
||||
except AttributeError:
|
||||
except AttributeError, KeyError:
|
||||
return ([], [])
|
||||
return (list(heating or []), list(cooling or []))
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["dsmr_parser"],
|
||||
"requirements": ["dsmr-parser==1.4.3"]
|
||||
"requirements": ["dsmr-parser==1.5.0"]
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ DUNEHD_PLAYER_SUPPORT: Final[MediaPlayerEntityFeature] = (
|
||||
| MediaPlayerEntityFeature.PLAY
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
| MediaPlayerEntityFeature.BROWSE_MEDIA
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -524,14 +524,10 @@ class EsphomeAssistSatellite(
|
||||
self._active_pipeline_index = 0
|
||||
|
||||
maybe_pipeline_index = 0
|
||||
while True:
|
||||
if not (ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index)):
|
||||
break
|
||||
|
||||
if not (ww_state := self.hass.states.get(ww_entity_id)):
|
||||
continue
|
||||
|
||||
if ww_state.state == wake_word_phrase:
|
||||
while ww_entity_id := self.get_wake_word_entity(maybe_pipeline_index):
|
||||
if (
|
||||
ww_state := self.hass.states.get(ww_entity_id)
|
||||
) and ww_state.state == wake_word_phrase:
|
||||
# First match
|
||||
self._active_pipeline_index = maybe_pipeline_index
|
||||
break
|
||||
|
||||
@@ -275,8 +275,11 @@ class FibaroController:
|
||||
# otherwise add the first visible device in the group
|
||||
# which is a hack, but solves a problem with FGT having
|
||||
# hidden compatibility devices before the real device
|
||||
if last_climate_parent != device.parent_fibaro_id or (
|
||||
device.has_endpoint_id and last_endpoint != device.endpoint_id
|
||||
# Second hack is for quickapps which have parent id 0 and no children
|
||||
if (
|
||||
last_climate_parent != device.parent_fibaro_id
|
||||
or (device.has_endpoint_id and last_endpoint != device.endpoint_id)
|
||||
or device.parent_fibaro_id == 0
|
||||
):
|
||||
_LOGGER.debug("Handle separately")
|
||||
self.fibaro_devices[platform].append(device)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260225.0"]
|
||||
"requirements": ["home-assistant-frontend==20260304.0"]
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
site_title = site["title"]
|
||||
|
||||
await self.async_set_unique_id(site["uuid"])
|
||||
await self.async_set_unique_id(site["site_uuid"])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return self.async_create_entry(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiogithubapi"],
|
||||
"requirements": ["aiogithubapi==24.6.0"]
|
||||
"requirements": ["aiogithubapi==26.0.0"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhive-integration==1.0.7"]
|
||||
"requirements": ["pyhive-integration==1.0.8"]
|
||||
}
|
||||
|
||||
@@ -88,6 +88,17 @@ class HomematicipHeatingGroup(HomematicipGenericEntity, ClimateEntity):
|
||||
if device.actualTemperature is None:
|
||||
self._simple_heating = self._first_radiator_thermostat
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Heating group available.
|
||||
|
||||
A heating group must be available, and should not be affected by the
|
||||
individual availability of group members.
|
||||
This allows controlling the temperature even when individual group
|
||||
members are not available.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device specific attributes."""
|
||||
|
||||
@@ -312,6 +312,17 @@ class HomematicipCoverShutterGroup(HomematicipGenericEntity, CoverEntity):
|
||||
device.modelType = f"HmIP-{post}"
|
||||
super().__init__(hap, device, post, is_multi_channel=False)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Cover shutter group available.
|
||||
|
||||
A cover shutter group must be available, and should not be affected by
|
||||
the individual availability of group members.
|
||||
This allows controlling the shutters even when individual group
|
||||
members are not available.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def current_cover_position(self) -> int | None:
|
||||
"""Return current position of cover."""
|
||||
|
||||
@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="energy_exported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="energy_imported",
|
||||
translation_key="energy_imported",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="frequency",
|
||||
|
||||
@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
|
||||
key="active_liter_lpm",
|
||||
translation_key="active_liter_lpm",
|
||||
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
|
||||
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
|
||||
value_fn=lambda data: data.measurement.active_liter_lpm,
|
||||
|
||||
@@ -901,7 +901,9 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
|
||||
)
|
||||
|
||||
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined):
|
||||
class PowerViewShadeDualOverlappedCombinedTilt(
|
||||
PowerViewShadeDualOverlappedCombined, PowerViewShadeWithTiltBase
|
||||
):
|
||||
"""Represent a shade that has a front sheer and rear opaque panel.
|
||||
|
||||
This equates to two shades being controlled by one motor.
|
||||
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
Type 10 - Duolite with 180° Tilt
|
||||
"""
|
||||
|
||||
# type
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PowerviewShadeUpdateCoordinator,
|
||||
device_info: PowerviewDeviceInfo,
|
||||
room_name: str,
|
||||
shade: BaseShade,
|
||||
name: str,
|
||||
) -> None:
|
||||
"""Initialize the shade."""
|
||||
super().__init__(coordinator, device_info, room_name, shade, name)
|
||||
self._attr_supported_features |= (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.SET_TILT_POSITION
|
||||
)
|
||||
if self._shade.is_supported(MOTION_STOP):
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||
self._max_tilt = self._shade.shade_limits.tilt_max
|
||||
|
||||
@property
|
||||
def transition_steps(self) -> int:
|
||||
"""Return the steps to make a move."""
|
||||
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
|
||||
tilt = self.positions.tilt
|
||||
return ceil(primary + secondary + tilt)
|
||||
|
||||
@callback
|
||||
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
|
||||
"""Return a ShadePosition."""
|
||||
return ShadePosition(
|
||||
tilt=target_hass_tilt_position,
|
||||
velocity=self.positions.velocity,
|
||||
)
|
||||
|
||||
@property
|
||||
def open_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
|
||||
|
||||
@property
|
||||
def close_tilt_position(self) -> ShadePosition:
|
||||
"""Return the open tilt position and required additional positions."""
|
||||
return replace(
|
||||
self._shade.close_position_tilt, velocity=self.positions.velocity
|
||||
)
|
||||
|
||||
|
||||
TYPE_TO_CLASSES = {
|
||||
0: (PowerViewShade,),
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"battery_charge_discharge_state": {
|
||||
"name": "Battery charge/discharge state",
|
||||
"state": {
|
||||
"charging": "Charging",
|
||||
"discharging": "Discharging",
|
||||
"charging": "[%key:common::state::charging%]",
|
||||
"discharging": "[%key:common::state::discharging%]",
|
||||
"static": "Static"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -43,6 +43,7 @@ from homeassistant.const import (
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, State, callback
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv, state as state_helper
|
||||
from homeassistant.helpers.entity_values import EntityValues
|
||||
@@ -61,6 +62,7 @@ from .const import (
|
||||
CLIENT_ERROR_V2,
|
||||
CODE_INVALID_INPUTS,
|
||||
COMPONENT_CONFIG_SCHEMA_CONNECTION,
|
||||
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS,
|
||||
CONF_API_VERSION,
|
||||
CONF_BUCKET,
|
||||
CONF_COMPONENT_CONFIG,
|
||||
@@ -79,7 +81,6 @@ from .const import (
|
||||
CONF_TAGS_ATTRIBUTES,
|
||||
CONNECTION_ERROR,
|
||||
DEFAULT_API_VERSION,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_HOST_V2,
|
||||
DEFAULT_MEASUREMENT_ATTR,
|
||||
DEFAULT_SSL_V2,
|
||||
@@ -104,6 +105,7 @@ from .const import (
|
||||
WRITE_ERROR,
|
||||
WROTE_MESSAGE,
|
||||
)
|
||||
from .issue import async_create_deprecated_yaml_issue
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -137,7 +139,7 @@ def create_influx_url(conf: dict) -> dict:
|
||||
|
||||
def validate_version_specific_config(conf: dict) -> dict:
|
||||
"""Ensure correct config fields are provided based on API version used."""
|
||||
if conf[CONF_API_VERSION] == API_VERSION_2:
|
||||
if conf.get(CONF_API_VERSION, DEFAULT_API_VERSION) == API_VERSION_2:
|
||||
if CONF_TOKEN not in conf:
|
||||
raise vol.Invalid(
|
||||
f"{CONF_TOKEN} and {CONF_BUCKET} are required when"
|
||||
@@ -193,32 +195,13 @@ _INFLUX_BASE_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
||||
}
|
||||
)
|
||||
|
||||
INFLUX_SCHEMA = vol.All(
|
||||
_INFLUX_BASE_SCHEMA.extend(COMPONENT_CONFIG_SCHEMA_CONNECTION),
|
||||
validate_version_specific_config,
|
||||
create_influx_url,
|
||||
INFLUX_SCHEMA = _INFLUX_BASE_SCHEMA.extend(
|
||||
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.All(
|
||||
cv.deprecated(CONF_API_VERSION),
|
||||
cv.deprecated(CONF_HOST),
|
||||
cv.deprecated(CONF_PATH),
|
||||
cv.deprecated(CONF_PORT),
|
||||
cv.deprecated(CONF_SSL),
|
||||
cv.deprecated(CONF_VERIFY_SSL),
|
||||
cv.deprecated(CONF_SSL_CA_CERT),
|
||||
cv.deprecated(CONF_USERNAME),
|
||||
cv.deprecated(CONF_PASSWORD),
|
||||
cv.deprecated(CONF_DB_NAME),
|
||||
cv.deprecated(CONF_TOKEN),
|
||||
cv.deprecated(CONF_ORG),
|
||||
cv.deprecated(CONF_BUCKET),
|
||||
INFLUX_SCHEMA,
|
||||
)
|
||||
},
|
||||
{DOMAIN: vol.All(INFLUX_SCHEMA, validate_version_specific_config)},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
@@ -499,23 +482,35 @@ def get_influx_connection( # noqa: C901
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the InfluxDB component."""
|
||||
conf = config.get(DOMAIN)
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
if conf is not None:
|
||||
if CONF_HOST not in conf and conf[CONF_API_VERSION] == DEFAULT_API_VERSION:
|
||||
conf[CONF_HOST] = DEFAULT_HOST
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=conf,
|
||||
)
|
||||
)
|
||||
hass.async_create_task(_async_setup(hass, config[DOMAIN]))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def _async_setup(hass: HomeAssistant, config: dict[str, Any]) -> None:
|
||||
"""Import YAML configuration into a config entry."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
if (
|
||||
result.get("type") is FlowResultType.ABORT
|
||||
and (reason := result["reason"]) != "single_instance_allowed"
|
||||
):
|
||||
async_create_deprecated_yaml_issue(hass, error=reason)
|
||||
return
|
||||
|
||||
# If we are here, the entry already exists (single instance allowed)
|
||||
if config.keys() & (
|
||||
{k.schema for k in COMPONENT_CONFIG_SCHEMA_CONNECTION} - {CONF_PRECISION}
|
||||
):
|
||||
async_create_deprecated_yaml_issue(hass)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: InfluxDBConfigEntry) -> bool:
|
||||
"""Set up InfluxDB from a config entry."""
|
||||
data = entry.data
|
||||
|
||||
@@ -31,7 +31,7 @@ from homeassistant.helpers.selector import (
|
||||
)
|
||||
from homeassistant.helpers.storage import STORAGE_DIR
|
||||
|
||||
from . import DOMAIN, get_influx_connection
|
||||
from . import DOMAIN, create_influx_url, get_influx_connection
|
||||
from .const import (
|
||||
API_VERSION_2,
|
||||
CONF_API_VERSION,
|
||||
@@ -40,8 +40,11 @@ from .const import (
|
||||
CONF_ORG,
|
||||
CONF_SSL_CA_CERT,
|
||||
DEFAULT_API_VERSION,
|
||||
DEFAULT_BUCKET,
|
||||
DEFAULT_DATABASE,
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_VERIFY_SSL,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -240,14 +243,17 @@ class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
host = import_data.get(CONF_HOST)
|
||||
database = import_data.get(CONF_DB_NAME)
|
||||
bucket = import_data.get(CONF_BUCKET)
|
||||
import_data = {**import_data}
|
||||
import_data.setdefault(CONF_API_VERSION, DEFAULT_API_VERSION)
|
||||
import_data.setdefault(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
|
||||
import_data.setdefault(CONF_DB_NAME, DEFAULT_DATABASE)
|
||||
import_data.setdefault(CONF_BUCKET, DEFAULT_BUCKET)
|
||||
|
||||
api_version = import_data.get(CONF_API_VERSION)
|
||||
ssl = import_data.get(CONF_SSL)
|
||||
api_version = import_data[CONF_API_VERSION]
|
||||
|
||||
if api_version == DEFAULT_API_VERSION:
|
||||
host = import_data.get(CONF_HOST, DEFAULT_HOST)
|
||||
database = import_data[CONF_DB_NAME]
|
||||
title = f"{database} ({host})"
|
||||
data = {
|
||||
CONF_API_VERSION: api_version,
|
||||
@@ -256,21 +262,23 @@ class InfluxDBConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME: import_data.get(CONF_USERNAME),
|
||||
CONF_PASSWORD: import_data.get(CONF_PASSWORD),
|
||||
CONF_DB_NAME: database,
|
||||
CONF_SSL: ssl,
|
||||
CONF_SSL: import_data.get(CONF_SSL),
|
||||
CONF_PATH: import_data.get(CONF_PATH),
|
||||
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
|
||||
CONF_VERIFY_SSL: import_data[CONF_VERIFY_SSL],
|
||||
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
|
||||
}
|
||||
else:
|
||||
create_influx_url(import_data) # Only modifies dict for api_version == 2
|
||||
bucket = import_data[CONF_BUCKET]
|
||||
url = import_data.get(CONF_URL)
|
||||
title = f"{bucket} ({url})"
|
||||
data = {
|
||||
CONF_API_VERSION: api_version,
|
||||
CONF_URL: import_data.get(CONF_URL),
|
||||
CONF_URL: url,
|
||||
CONF_TOKEN: import_data.get(CONF_TOKEN),
|
||||
CONF_ORG: import_data.get(CONF_ORG),
|
||||
CONF_BUCKET: bucket,
|
||||
CONF_VERIFY_SSL: import_data.get(CONF_VERIFY_SSL),
|
||||
CONF_VERIFY_SSL: import_data[CONF_VERIFY_SSL],
|
||||
CONF_SSL_CA_CERT: import_data.get(CONF_SSL_CA_CERT),
|
||||
}
|
||||
|
||||
|
||||
@@ -154,3 +154,14 @@ COMPONENT_CONFIG_SCHEMA_CONNECTION = {
|
||||
vol.Inclusive(CONF_ORG, "v2_authentication"): cv.string,
|
||||
vol.Optional(CONF_BUCKET, default=DEFAULT_BUCKET): cv.string,
|
||||
}
|
||||
|
||||
# Same keys without defaults, used in CONFIG_SCHEMA to validate
|
||||
# without injecting default values (so we can detect explicit keys).
|
||||
COMPONENT_CONFIG_SCHEMA_CONNECTION_VALIDATORS = {
|
||||
(
|
||||
vol.Optional(k.schema)
|
||||
if isinstance(k, vol.Optional) and k.default is not vol.UNDEFINED
|
||||
else k
|
||||
): v
|
||||
for k, v in COMPONENT_CONFIG_SCHEMA_CONNECTION.items()
|
||||
}
|
||||
|
||||
34
homeassistant/components/influxdb/issue.py
Normal file
34
homeassistant/components/influxdb/issue.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Issues for InfluxDB integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_deprecated_yaml_issue(
|
||||
hass: HomeAssistant, *, error: str | None = None
|
||||
) -> None:
|
||||
"""Create a repair issue for deprecated YAML connection configuration."""
|
||||
if error is None:
|
||||
issue_id = "deprecated_yaml"
|
||||
severity = IssueSeverity.WARNING
|
||||
else:
|
||||
issue_id = f"deprecated_yaml_import_issue_{error}"
|
||||
severity = IssueSeverity.ERROR
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
issue_domain=DOMAIN,
|
||||
breaks_in_ha_version="2026.9.0",
|
||||
severity=severity,
|
||||
translation_key=issue_id,
|
||||
translation_placeholders={
|
||||
"domain": DOMAIN,
|
||||
"url": f"/config/integrations/dashboard/add?domain={DOMAIN}",
|
||||
},
|
||||
)
|
||||
@@ -7,7 +7,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/influxdb",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["influxdb", "influxdb_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["influxdb==5.3.1", "influxdb-client==1.50.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -54,5 +54,31 @@
|
||||
"title": "Choose InfluxDB version"
|
||||
}
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed. Your existing YAML connection configuration has been imported into the UI automatically.\n\nRemove the `{domain}` connection and authentication keys from your `configuration.yaml` file and restart Home Assistant to fix this issue. Other options like `include`, `exclude`, and `tags` remain in YAML for now. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "The InfluxDB YAML configuration is being removed"
|
||||
},
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because Home Assistant could not connect to the InfluxDB server.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "Failed to import InfluxDB YAML configuration"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_auth": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because the provided credentials are invalid.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_invalid_database": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed because the specified database was not found.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_ssl_error": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed due to an SSL certificate error.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
|
||||
},
|
||||
"deprecated_yaml_import_issue_unknown": {
|
||||
"description": "Configuring InfluxDB connection settings using YAML is being removed but the import failed due to an unknown error.\n\nPlease correct your YAML configuration and restart Home Assistant.\n\nAlternatively you can remove the `{domain}` connection and authentication keys from your `configuration.yaml` file and continue to [set up the integration]({url}) manually. \n\nThe following keys should be removed:\n- `api_version`\n- `host`\n- `port`\n- `ssl`\n- `verify_ssl`\n- `ssl_ca_cert`\n- `username`\n- `password`\n- `database`\n- `token`\n- `organization`\n- `bucket`\n- `path`",
|
||||
"title": "[%key:component::influxdb::issues::deprecated_yaml_import_issue_cannot_connect::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the device specific state attributes."""
|
||||
attrs = {}
|
||||
if self._outdoor_temp:
|
||||
if self._outdoor_temp is not None:
|
||||
attrs["outdoor_temp"] = self._outdoor_temp
|
||||
if self._power_consumption_heat:
|
||||
if self._power_consumption_heat is not None:
|
||||
attrs["power_consumption_heat_kw"] = round(
|
||||
self._power_consumption_heat / 1000, 1
|
||||
)
|
||||
if self._power_consumption_cool:
|
||||
if self._power_consumption_cool is not None:
|
||||
attrs["power_consumption_cool_kw"] = round(
|
||||
self._power_consumption_cool / 1000, 1
|
||||
)
|
||||
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
|
||||
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
|
||||
await self.async_set_hvac_mode(hvac_mode)
|
||||
|
||||
if temperature := kwargs.get(ATTR_TEMPERATURE):
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
|
||||
await self._controller.set_temperature(self._device_id, temperature)
|
||||
self._attr_target_temperature = temperature
|
||||
@@ -271,7 +271,7 @@ class IntesisAC(ClimateEntity):
|
||||
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
|
||||
|
||||
# Send the temperature again in case changing modes has changed it
|
||||
if self._attr_target_temperature:
|
||||
if self._attr_target_temperature is not None:
|
||||
await self._controller.set_temperature(
|
||||
self._device_id, self._attr_target_temperature
|
||||
)
|
||||
|
||||
@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
|
||||
native_max_value=MAX_TEMP,
|
||||
native_min_value_f=MIN_TEMP_F,
|
||||
native_max_value_f=MAX_TEMP_F,
|
||||
native_step=5,
|
||||
native_step=1,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aionotify", "evdev"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"]
|
||||
"requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
|
||||
from xknx.dpt.dpt_16 import DPTString
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.const import UnitOfReactiveEnergy
|
||||
|
||||
HaDptClass = Literal["numeric", "enum", "complex", "string"]
|
||||
|
||||
@@ -36,7 +37,7 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
|
||||
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
|
||||
sub=dpt_class.dpt_sub_number,
|
||||
name=dpt_class.value_type,
|
||||
unit=dpt_class.unit,
|
||||
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
|
||||
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
|
||||
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
|
||||
)
|
||||
@@ -77,13 +78,13 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"12.1200": SensorDeviceClass.VOLUME,
|
||||
"12.1201": SensorDeviceClass.VOLUME,
|
||||
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"13.010": SensorDeviceClass.ENERGY,
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.013": SensorDeviceClass.ENERGY,
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY,
|
||||
"13.016": SensorDeviceClass.ENERGY,
|
||||
"13.1200": SensorDeviceClass.VOLUME,
|
||||
"13.1201": SensorDeviceClass.VOLUME,
|
||||
"13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
|
||||
"13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
|
||||
"13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
|
||||
"13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
|
||||
"14.010": SensorDeviceClass.AREA,
|
||||
"14.019": SensorDeviceClass.CURRENT,
|
||||
"14.027": SensorDeviceClass.VOLTAGE,
|
||||
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.030": SensorDeviceClass.VOLTAGE,
|
||||
"14.031": SensorDeviceClass.ENERGY,
|
||||
"14.033": SensorDeviceClass.FREQUENCY,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE,
|
||||
"14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
|
||||
"14.039": SensorDeviceClass.DISTANCE,
|
||||
"14.051": SensorDeviceClass.WEIGHT,
|
||||
"14.056": SensorDeviceClass.POWER,
|
||||
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
|
||||
"14.068": SensorDeviceClass.TEMPERATURE,
|
||||
"14.069": SensorDeviceClass.TEMPERATURE,
|
||||
"14.070": SensorDeviceClass.TEMPERATURE_DELTA,
|
||||
"14.076": SensorDeviceClass.VOLUME,
|
||||
"14.076": SensorDeviceClass.VOLUME, # DPTVolume
|
||||
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
"14.080": SensorDeviceClass.APPARENT_POWER,
|
||||
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
|
||||
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
|
||||
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
|
||||
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
|
||||
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy
|
||||
"13.013": SensorStateClass.TOTAL, # DPTActiveEnergykWh
|
||||
"13.015": SensorStateClass.TOTAL, # DPTReactiveEnergykVARh
|
||||
"13.016": SensorStateClass.TOTAL, # DPTActiveEnergyMWh
|
||||
"13.1200": SensorStateClass.TOTAL, # DPTDeltaVolumeLiquidLitre
|
||||
"13.1201": SensorStateClass.TOTAL, # DPTDeltaVolumeM3
|
||||
"14.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
|
||||
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
|
||||
"14.051": SensorStateClass.TOTAL, # DPTMass
|
||||
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
|
||||
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
|
||||
"14.076": SensorStateClass.TOTAL, # DPTVolume
|
||||
"17.001": None, # DPTSceneNumber
|
||||
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
|
||||
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
|
||||
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte
|
||||
}
|
||||
|
||||
_sensor_unit_overrides: Mapping[str, str] = {
|
||||
"13.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy (VARh in KNX)
|
||||
"13.015": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergykVARh (kVARh in KNX)
|
||||
"29.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy8Byte (VARh in KNX)
|
||||
}
|
||||
|
||||
|
||||
def _get_sensor_state_class(
|
||||
ha_dpt_class: HaDptClass, dpt_number_str: str
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"requirements": [
|
||||
"xknx==3.15.0",
|
||||
"xknxproject==3.8.2",
|
||||
"knx-frontend==2026.2.25.165736"
|
||||
"knx-frontend==2026.3.2.183756"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from chip.clusters import Objects as clusters
|
||||
@@ -26,6 +27,8 @@ from .entity import MatterEntity, MatterEntityDescription
|
||||
from .helpers import get_matter
|
||||
from .models import MatterDiscoverySchema
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OperationalState(IntEnum):
|
||||
"""Operational State of the vacuum cleaner.
|
||||
@@ -168,8 +171,9 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
segments: dict[str, Segment] = {}
|
||||
for area in supported_areas:
|
||||
area_name = None
|
||||
if area.areaInfo and area.areaInfo.locationInfo:
|
||||
area_name = area.areaInfo.locationInfo.locationName
|
||||
location_info = area.areaInfo.locationInfo
|
||||
if location_info not in (None, clusters.NullValue):
|
||||
area_name = location_info.locationName
|
||||
|
||||
if area_name:
|
||||
segment_id = str(area.areaID)
|
||||
@@ -206,10 +210,11 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
|
||||
if (
|
||||
response
|
||||
and response.status != clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess
|
||||
and response["status"]
|
||||
!= clusters.ServiceArea.Enums.SelectAreasStatus.kSuccess
|
||||
):
|
||||
raise HomeAssistantError(
|
||||
f"Failed to select areas: {response.statusText or response.status.name}"
|
||||
f"Failed to select areas: {response['statusText'] or response['status']}"
|
||||
)
|
||||
|
||||
await self.send_device_command(
|
||||
@@ -252,9 +257,18 @@ class MatterVacuum(MatterEntity, StateVacuumEntity):
|
||||
VacuumEntityFeature.CLEAN_AREA in self.supported_features
|
||||
and self.registry_entry is not None
|
||||
and (last_seen_segments := self.last_seen_segments) is not None
|
||||
and self._current_segments != {s.id: s for s in last_seen_segments}
|
||||
# Ignore empty segments; some devices transiently
|
||||
# report an empty list before sending the real one.
|
||||
and (current_segments := self._current_segments)
|
||||
):
|
||||
self.async_create_segments_issue()
|
||||
last_seen_by_id = {s.id: s for s in last_seen_segments}
|
||||
if current_segments != last_seen_by_id:
|
||||
_LOGGER.debug(
|
||||
"Vacuum segments changed: last_seen=%s, current=%s",
|
||||
last_seen_by_id,
|
||||
current_segments,
|
||||
)
|
||||
self.async_create_segments_issue()
|
||||
|
||||
@callback
|
||||
def _calculate_features(self) -> None:
|
||||
|
||||
@@ -617,11 +617,11 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
evaporate_water = 327
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
drying = 357
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359
|
||||
prove_dough = 359, 2023
|
||||
low_temperature_cooking = 360
|
||||
steam_cooking = 361
|
||||
steam_cooking = 8, 361
|
||||
keeping_warm = 362
|
||||
apple_sponge = 364
|
||||
apple_pie = 365
|
||||
@@ -668,9 +668,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
saddle_of_roebuck = 456
|
||||
salmon_fillet = 461
|
||||
potato_cheese_gratin = 464
|
||||
trout = 486
|
||||
carp = 491
|
||||
salmon_trout = 492
|
||||
trout = 486, 2224
|
||||
carp = 491, 2233
|
||||
salmon_trout = 492, 2241
|
||||
springform_tin_15cm = 496
|
||||
springform_tin_20cm = 497
|
||||
springform_tin_25cm = 498
|
||||
@@ -736,137 +736,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pork_belly = 701
|
||||
pikeperch_fillet_with_vegetables = 702
|
||||
steam_bake = 99001
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for steam oven micro combo."""
|
||||
|
||||
no_program = 0, -1
|
||||
steam_cooking = 8
|
||||
microwave = 19
|
||||
popcorn = 53
|
||||
quick_mw = 54
|
||||
sous_vide = 72
|
||||
eco_steam_cooking = 75
|
||||
rapid_steam_cooking = 77
|
||||
descale = 326
|
||||
menu_cooking = 330
|
||||
reheating_with_steam = 2018
|
||||
defrosting_with_steam = 2019
|
||||
blanching = 2020
|
||||
bottling = 2021
|
||||
sterilize_crockery = 2022
|
||||
prove_dough = 2023
|
||||
soak = 2027
|
||||
reheating_with_microwave = 2029
|
||||
defrosting_with_microwave = 2030
|
||||
@@ -1020,18 +898,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
gilt_head_bream_fillet = 2220
|
||||
codfish_piece = 2221, 2232
|
||||
codfish_fillet = 2222, 2231
|
||||
trout = 2224
|
||||
pike_fillet = 2225
|
||||
pike_piece = 2226
|
||||
halibut_fillet_2_cm = 2227
|
||||
halibut_fillet_3_cm = 2230
|
||||
carp = 2233
|
||||
salmon_fillet_2_cm = 2234
|
||||
salmon_fillet_3_cm = 2235
|
||||
salmon_steak_2_cm = 2238
|
||||
salmon_steak_3_cm = 2239
|
||||
salmon_piece = 2240
|
||||
salmon_trout = 2241
|
||||
iridescent_shark_fillet = 2244
|
||||
red_snapper_fillet_2_cm = 2245
|
||||
red_snapper_fillet_3_cm = 2248
|
||||
@@ -1268,6 +1143,116 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
|
||||
round_grain_rice_general_rapid_steam_cooking = 3411
|
||||
|
||||
|
||||
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for dish warmers."""
|
||||
|
||||
no_program = 0, -1
|
||||
warm_cups_glasses = 1
|
||||
warm_dishes_plates = 2
|
||||
keep_warm = 3
|
||||
slow_roasting = 4
|
||||
|
||||
|
||||
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for robot vacuum cleaners."""
|
||||
|
||||
no_program = 0, -1
|
||||
auto = 1
|
||||
spot = 2
|
||||
turbo = 3
|
||||
silent = 4
|
||||
|
||||
|
||||
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
|
||||
"""Program Id codes for coffee systems."""
|
||||
|
||||
no_program = 0, -1
|
||||
|
||||
check_appliance = 17004
|
||||
|
||||
# profile 1
|
||||
ristretto = 24000, 24032, 24064, 24096, 24128
|
||||
espresso = 24001, 24033, 24065, 24097, 24129
|
||||
coffee = 24002, 24034, 24066, 24098, 24130
|
||||
long_coffee = 24003, 24035, 24067, 24099, 24131
|
||||
cappuccino = 24004, 24036, 24068, 24100, 24132
|
||||
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
|
||||
latte_macchiato = 24006, 24038, 24070, 24102, 24134
|
||||
espresso_macchiato = 24007, 24039, 24071, 24135
|
||||
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
|
||||
caffe_latte = 24009, 24041, 24073, 24105, 24137
|
||||
flat_white = 24012, 24044, 24076, 24108, 24140
|
||||
very_hot_water = 24013, 24045, 24077, 24109, 24141
|
||||
hot_water = 24014, 24046, 24078, 24110, 24142
|
||||
hot_milk = 24015, 24047, 24079, 24111, 24143
|
||||
milk_foam = 24016, 24048, 24080, 24112, 24144
|
||||
black_tea = 24017, 24049, 24081, 24113, 24145
|
||||
herbal_tea = 24018, 24050, 24082, 24114, 24146
|
||||
fruit_tea = 24019, 24051, 24083, 24115, 24147
|
||||
green_tea = 24020, 24052, 24084, 24116, 24148
|
||||
white_tea = 24021, 24053, 24085, 24117, 24149
|
||||
japanese_tea = 24022, 29054, 24086, 24118, 24150
|
||||
# special programs
|
||||
coffee_pot = 24400
|
||||
barista_assistant = 24407
|
||||
# machine settings menu
|
||||
appliance_settings = (
|
||||
16016, # display brightness
|
||||
16018, # volume
|
||||
16019, # buttons volume
|
||||
16020, # child lock
|
||||
16021, # water hardness
|
||||
16027, # welcome sound
|
||||
16033, # connection status
|
||||
16035, # remote control
|
||||
16037, # remote update
|
||||
24500, # total dispensed
|
||||
24502, # lights appliance on
|
||||
24503, # lights appliance off
|
||||
24504, # turn off lights after
|
||||
24506, # altitude
|
||||
24513, # performance mode
|
||||
24516, # turn off after
|
||||
24537, # advanced mode
|
||||
24542, # tea timer
|
||||
24549, # total coffee dispensed
|
||||
24550, # total tea dispensed
|
||||
24551, # total ristretto
|
||||
24552, # total cappuccino
|
||||
24553, # total espresso
|
||||
24554, # total coffee
|
||||
24555, # total long coffee
|
||||
24556, # total italian cappuccino
|
||||
24557, # total latte macchiato
|
||||
24558, # total caffe latte
|
||||
24560, # total espresso macchiato
|
||||
24562, # total flat white
|
||||
24563, # total coffee with milk
|
||||
24564, # total black tea
|
||||
24565, # total herbal tea
|
||||
24566, # total fruit tea
|
||||
24567, # total green tea
|
||||
24568, # total white tea
|
||||
24569, # total japanese tea
|
||||
24571, # total milk foam
|
||||
24572, # total hot milk
|
||||
24573, # total hot water
|
||||
24574, # total very hot water
|
||||
24575, # counter to descaling
|
||||
24576, # counter to brewing unit degreasing
|
||||
24800, # maintenance
|
||||
24801, # profiles settings menu
|
||||
24813, # add profile
|
||||
)
|
||||
appliance_rinse = 24750, 24759, 24773, 24787, 24788
|
||||
intermediate_rinsing = 24758
|
||||
automatic_maintenance = 24778
|
||||
descaling = 24751
|
||||
brewing_unit_degrease = 24753
|
||||
milk_pipework_rinse = 24754
|
||||
milk_pipework_clean = 24789
|
||||
|
||||
|
||||
PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
|
||||
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
|
||||
@@ -1278,7 +1263,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
|
||||
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId,
|
||||
MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
|
||||
MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
|
||||
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
|
||||
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,
|
||||
|
||||
@@ -474,6 +474,7 @@
|
||||
"drain_spin": "Drain/spin",
|
||||
"drop_cookies_1_tray": "Drop cookies (1 tray)",
|
||||
"drop_cookies_2_trays": "Drop cookies (2 trays)",
|
||||
"drying": "Drying",
|
||||
"duck": "Duck",
|
||||
"dutch_hash": "Dutch hash",
|
||||
"easy_care": "Easy care",
|
||||
|
||||
@@ -120,6 +120,7 @@ class MobileAppNotificationService(BaseNotificationService):
|
||||
|
||||
local_push_channels = self.hass.data[DOMAIN][DATA_PUSH_CHANNEL]
|
||||
|
||||
failed_targets = []
|
||||
for target in targets:
|
||||
registration = self.hass.data[DOMAIN][DATA_CONFIG_ENTRIES][target].data
|
||||
|
||||
@@ -134,12 +135,16 @@ class MobileAppNotificationService(BaseNotificationService):
|
||||
|
||||
# Test if local push only.
|
||||
if ATTR_PUSH_URL not in registration[ATTR_APP_DATA]:
|
||||
raise HomeAssistantError(
|
||||
"Device not connected to local push notifications"
|
||||
)
|
||||
failed_targets.append(target)
|
||||
continue
|
||||
|
||||
await self._async_send_remote_message_target(target, registration, data)
|
||||
|
||||
if failed_targets:
|
||||
raise HomeAssistantError(
|
||||
f"Device(s) with webhook id(s) {', '.join(failed_targets)} not connected to local push notifications"
|
||||
)
|
||||
|
||||
async def _async_send_remote_message_target(self, target, registration, data):
|
||||
"""Send a message to a target."""
|
||||
app_data = registration[ATTR_APP_DATA]
|
||||
|
||||
@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_PORT = 4999
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
||||
DEFAULT_INFER_ARMING_STATE = False
|
||||
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import asyncio
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
|
||||
from aiohttp import ClientError, ClientResponseError, web
|
||||
from aiohttp import ClientError, web
|
||||
from google_nest_sdm.camera_traits import CameraClipPreviewTrait
|
||||
from google_nest_sdm.device import Device
|
||||
from google_nest_sdm.device_manager import DeviceManager
|
||||
@@ -43,6 +43,8 @@ from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
HomeAssistantError,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
Unauthorized,
|
||||
)
|
||||
from homeassistant.helpers import (
|
||||
@@ -253,11 +255,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
|
||||
auth = await api.new_auth(hass, entry)
|
||||
try:
|
||||
await auth.async_get_access_token()
|
||||
except ClientResponseError as err:
|
||||
if 400 <= err.status < 500:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="reauth_required"
|
||||
) from err
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN, translation_key="reauth_required"
|
||||
) from err
|
||||
except OAuth2TokenRequestError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN, translation_key="auth_server_error"
|
||||
) from err
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -10,5 +10,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["onedrive_personal_sdk"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["onedrive-personal-sdk==0.1.4"]
|
||||
"requirements": ["onedrive-personal-sdk==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -512,6 +512,11 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
options.pop(CONF_WEB_SEARCH_REGION, None)
|
||||
options.pop(CONF_WEB_SEARCH_COUNTRY, None)
|
||||
options.pop(CONF_WEB_SEARCH_TIMEZONE, None)
|
||||
if (
|
||||
user_input.get(CONF_CODE_INTERPRETER)
|
||||
and user_input.get(CONF_REASONING_EFFORT) == "minimal"
|
||||
):
|
||||
errors[CONF_CODE_INTERPRETER] = "code_interpreter_minimal_reasoning"
|
||||
|
||||
options.update(user_input)
|
||||
if not errors:
|
||||
@@ -539,15 +544,15 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
|
||||
if not model.startswith(("o", "gpt-5")) or model.startswith("gpt-5-pro"):
|
||||
return []
|
||||
|
||||
MODELS_REASONING_MAP = {
|
||||
models_reasoning_map: dict[str | tuple[str, ...], list[str]] = {
|
||||
"gpt-5.2-pro": ["medium", "high", "xhigh"],
|
||||
"gpt-5.2": ["none", "low", "medium", "high", "xhigh"],
|
||||
("gpt-5.2", "gpt-5.3"): ["none", "low", "medium", "high", "xhigh"],
|
||||
"gpt-5.1": ["none", "low", "medium", "high"],
|
||||
"gpt-5": ["minimal", "low", "medium", "high"],
|
||||
"": ["low", "medium", "high"], # The default case
|
||||
}
|
||||
|
||||
for prefix, options in MODELS_REASONING_MAP.items():
|
||||
for prefix, options in models_reasoning_map.items():
|
||||
if model.startswith(prefix):
|
||||
return options
|
||||
return [] # pragma: no cover
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
},
|
||||
"entry_type": "AI task",
|
||||
"error": {
|
||||
"code_interpreter_minimal_reasoning": "[%key:component::openai_conversation::config_subentries::conversation::error::code_interpreter_minimal_reasoning%]",
|
||||
"model_not_supported": "[%key:component::openai_conversation::config_subentries::conversation::error::model_not_supported%]",
|
||||
"web_search_minimal_reasoning": "[%key:component::openai_conversation::config_subentries::conversation::error::web_search_minimal_reasoning%]"
|
||||
},
|
||||
@@ -93,6 +94,7 @@
|
||||
},
|
||||
"entry_type": "Conversation agent",
|
||||
"error": {
|
||||
"code_interpreter_minimal_reasoning": "Code interpreter is not supported with minimal reasoning effort",
|
||||
"model_not_supported": "This model is not supported, please select a different model",
|
||||
"web_search_minimal_reasoning": "Web search is currently not supported with minimal reasoning effort"
|
||||
},
|
||||
|
||||
@@ -69,7 +69,7 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
if self.source == SOURCE_USER:
|
||||
return self.async_create_entry(
|
||||
title="Overseerr",
|
||||
title="Seerr",
|
||||
data={
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"domain": "overseerr",
|
||||
"name": "Overseerr",
|
||||
"name": "Seerr",
|
||||
"after_dependencies": ["cloud"],
|
||||
"codeowners": ["@joostlek", "@AmGarera"],
|
||||
"config_flow": true,
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
"url": "[%key:common::config_flow::data::url%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "The API key of the Overseerr instance.",
|
||||
"url": "The URL of the Overseerr instance."
|
||||
"api_key": "The API key of the Seerr instance.",
|
||||
"url": "The URL of the Seerr instance."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@
|
||||
"message": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
},
|
||||
"connection_error": {
|
||||
"message": "Error connecting to the Overseerr instance: {error}"
|
||||
"message": "Error connecting to the Seerr instance: {error}"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -137,11 +137,11 @@
|
||||
},
|
||||
"services": {
|
||||
"get_requests": {
|
||||
"description": "Retrieves a list of media requests from Overseerr.",
|
||||
"description": "Retrieves a list of media requests from Seerr.",
|
||||
"fields": {
|
||||
"config_entry_id": {
|
||||
"description": "The Overseerr instance to get requests from.",
|
||||
"name": "Overseerr instance"
|
||||
"description": "The Seerr instance to get requests from.",
|
||||
"name": "Seerr instance"
|
||||
},
|
||||
"requested_by": {
|
||||
"description": "Filter the requests by the user ID that requested them.",
|
||||
|
||||
@@ -137,11 +137,10 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
|
||||
|
||||
_attr_effect: str
|
||||
_attr_translation_key = "ambilight"
|
||||
_attr_supported_color_modes = {ColorMode.HS}
|
||||
_attr_supported_features = LightEntityFeature.EFFECT
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PhilipsTVDataUpdateCoordinator,
|
||||
) -> None:
|
||||
def __init__(self, coordinator: PhilipsTVDataUpdateCoordinator) -> None:
|
||||
"""Initialize light."""
|
||||
self._tv = coordinator.api
|
||||
self._hs = None
|
||||
@@ -150,8 +149,6 @@ class PhilipsTVLightEntity(PhilipsJsEntity, LightEntity):
|
||||
self._last_selected_effect: AmbilightEffect | None = None
|
||||
super().__init__(coordinator)
|
||||
|
||||
self._attr_supported_color_modes = {ColorMode.HS, ColorMode.ONOFF}
|
||||
self._attr_supported_features = LightEntityFeature.EFFECT
|
||||
self._attr_unique_id = coordinator.unique_id
|
||||
|
||||
self._update_from_coordinator()
|
||||
|
||||
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import PortainerConfigEntry
|
||||
from .const import CONTAINER_STATE_RUNNING, STACK_STATUS_ACTIVE
|
||||
from .coordinator import PortainerContainerData, PortainerCoordinator
|
||||
from .coordinator import PortainerContainerData
|
||||
from .entity import (
|
||||
PortainerContainerEntity,
|
||||
PortainerCoordinatorData,
|
||||
@@ -165,18 +165,6 @@ class PortainerEndpointSensor(PortainerEndpointEntity, BinarySensorEntity):
|
||||
|
||||
entity_description: PortainerEndpointBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerEndpointBinarySensorEntityDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize Portainer endpoint binary sensor entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -188,19 +176,6 @@ class PortainerContainerSensor(PortainerContainerEntity, BinarySensorEntity):
|
||||
|
||||
entity_description: PortainerContainerBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerContainerBinarySensorEntityDescription,
|
||||
device_info: PortainerContainerData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer container sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
@@ -212,19 +187,6 @@ class PortainerStackSensor(PortainerStackEntity, BinarySensorEntity):
|
||||
|
||||
entity_description: PortainerStackBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerStackBinarySensorEntityDescription,
|
||||
device_info: PortainerStackData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer stack sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return true if the binary sensor is on."""
|
||||
|
||||
@@ -167,18 +167,6 @@ class PortainerEndpointButton(PortainerEndpointEntity, PortainerBaseButton):
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerButtonDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer endpoint button entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the endpoint button press action."""
|
||||
await self.entity_description.press_action(
|
||||
@@ -191,19 +179,6 @@ class PortainerContainerButton(PortainerContainerEntity, PortainerBaseButton):
|
||||
|
||||
entity_description: PortainerButtonDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerButtonDescription,
|
||||
device_info: PortainerContainerData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer button entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Call the container button press action."""
|
||||
await self.entity_description.press_action(
|
||||
|
||||
@@ -4,6 +4,7 @@ from yarl import URL
|
||||
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
@@ -26,11 +27,13 @@ class PortainerEndpointEntity(PortainerCoordinatorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_info: PortainerCoordinatorData,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize a Portainer endpoint."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._device_info = device_info
|
||||
self.device_id = device_info.endpoint.id
|
||||
self._attr_device_info = DeviceInfo(
|
||||
@@ -45,6 +48,7 @@ class PortainerEndpointEntity(PortainerCoordinatorEntity):
|
||||
name=device_info.endpoint.name,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -57,12 +61,14 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_info: PortainerContainerData,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
device_info: PortainerContainerData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize a Portainer container."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._device_info = device_info
|
||||
self.device_id = self._device_info.container.id
|
||||
self.endpoint_id = via_device.endpoint.id
|
||||
@@ -91,13 +97,14 @@ class PortainerContainerEntity(PortainerCoordinatorEntity):
|
||||
# else it's the endpoint
|
||||
via_device=(
|
||||
DOMAIN,
|
||||
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_{device_info.stack.name}"
|
||||
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_stack_{device_info.stack.id}"
|
||||
if device_info.stack
|
||||
else f"{coordinator.config_entry.entry_id}_{self.endpoint_id}",
|
||||
),
|
||||
translation_key=None if self.device_name else "unknown_container",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -119,12 +126,14 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
device_info: PortainerStackData,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
device_info: PortainerStackData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize a Portainer stack."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = entity_description
|
||||
self._device_info = device_info
|
||||
self.stack_id = device_info.stack.id
|
||||
self.device_name = device_info.stack.name
|
||||
@@ -135,7 +144,7 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
|
||||
identifiers={
|
||||
(
|
||||
DOMAIN,
|
||||
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_{self.device_name}",
|
||||
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}_stack_{self.stack_id}",
|
||||
)
|
||||
},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
@@ -149,6 +158,7 @@ class PortainerStackEntity(PortainerCoordinatorEntity):
|
||||
f"{coordinator.config_entry.entry_id}_{self.endpoint_id}",
|
||||
),
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.stack_id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyportainer==1.0.28"]
|
||||
"requirements": ["pyportainer==1.0.31"]
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ from .const import STACK_TYPE_COMPOSE, STACK_TYPE_KUBERNETES, STACK_TYPE_SWARM
|
||||
from .coordinator import (
|
||||
PortainerConfigEntry,
|
||||
PortainerContainerData,
|
||||
PortainerCoordinator,
|
||||
PortainerStackData,
|
||||
)
|
||||
from .entity import (
|
||||
@@ -398,19 +397,6 @@ class PortainerContainerSensor(PortainerContainerEntity, SensorEntity):
|
||||
|
||||
entity_description: PortainerContainerSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerContainerSensorEntityDescription,
|
||||
device_info: PortainerContainerData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer container sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -422,18 +408,6 @@ class PortainerEndpointSensor(PortainerEndpointEntity, SensorEntity):
|
||||
|
||||
entity_description: PortainerEndpointSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerEndpointSensorEntityDescription,
|
||||
device_info: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer endpoint sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -446,19 +420,6 @@ class PortainerStackSensor(PortainerStackEntity, SensorEntity):
|
||||
|
||||
entity_description: PortainerStackSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerStackSensorEntityDescription,
|
||||
device_info: PortainerStackData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer stack sensor."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -167,19 +167,6 @@ class PortainerContainerSwitch(PortainerContainerEntity, SwitchEntity):
|
||||
|
||||
entity_description: PortainerSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerSwitchEntityDescription,
|
||||
device_info: PortainerContainerData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer container switch."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{self.device_name}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the device."""
|
||||
@@ -209,19 +196,6 @@ class PortainerStackSwitch(PortainerStackEntity, SwitchEntity):
|
||||
|
||||
entity_description: PortainerStackSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PortainerCoordinator,
|
||||
entity_description: PortainerStackSwitchEntityDescription,
|
||||
device_info: PortainerStackData,
|
||||
via_device: PortainerCoordinatorData,
|
||||
) -> None:
|
||||
"""Initialize the Portainer stack switch."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(device_info, coordinator, via_device)
|
||||
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{device_info.stack.id}_{entity_description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool | None:
|
||||
"""Return the state of the device."""
|
||||
|
||||
@@ -49,12 +49,12 @@ class PowerfoxLocalDataUpdateCoordinator(DataUpdateCoordinator[LocalResponse]):
|
||||
except PowerfoxAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": str(err)},
|
||||
translation_key="auth_failed",
|
||||
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
|
||||
) from err
|
||||
except PowerfoxConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
translation_key="connection_error",
|
||||
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
|
||||
) from err
|
||||
|
||||
@@ -56,11 +56,11 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"invalid_auth": {
|
||||
"message": "Error while authenticating with the device: {error}"
|
||||
"auth_failed": {
|
||||
"message": "Authentication with the Poweropti device at {host} failed. Please check your API key."
|
||||
},
|
||||
"update_failed": {
|
||||
"message": "Error while updating the device: {error}"
|
||||
"connection_error": {
|
||||
"message": "Could not connect to the Poweropti device at {host}. Please check if the device is online and reachable."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,13 @@ from homeassistant.components.button import (
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .helpers import is_granted
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -276,6 +277,11 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes"):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_node_power",
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
@@ -303,6 +309,11 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
@@ -331,6 +342,12 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the container button action via executor."""
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms"):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_permission_vm_lxc_power",
|
||||
)
|
||||
await self.hass.async_add_executor_job(
|
||||
self.entity_description.press_action,
|
||||
self.coordinator,
|
||||
|
||||
@@ -17,3 +17,5 @@ DEFAULT_VERIFY_SSL = True
|
||||
TYPE_VM = 0
|
||||
TYPE_CONTAINER = 1
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
PERM_POWER = "VM.PowerMgmt"
|
||||
|
||||
@@ -70,6 +70,7 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
self.known_nodes: set[str] = set()
|
||||
self.known_vms: set[tuple[str, int]] = set()
|
||||
self.known_containers: set[tuple[str, int]] = set()
|
||||
self.permissions: dict[str, dict[str, int]] = {}
|
||||
|
||||
self.new_nodes_callbacks: list[Callable[[list[ProxmoxNodeData]], None]] = []
|
||||
self.new_vms_callbacks: list[
|
||||
@@ -101,11 +102,21 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
translation_key="timeout_connect",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except ResourceException as err:
|
||||
except ProxmoxServerError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="api_error_details",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except ProxmoxPermissionsError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="permissions_error",
|
||||
) from err
|
||||
except ProxmoxNodesNotFoundError as err:
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_nodes_found",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
raise ConfigEntryError(
|
||||
@@ -143,7 +154,6 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="no_nodes_found",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except requests.exceptions.ConnectionError as err:
|
||||
raise UpdateFailed(
|
||||
@@ -180,7 +190,19 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
password=self.config_entry.data[CONF_PASSWORD],
|
||||
verify_ssl=self.config_entry.data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
|
||||
)
|
||||
self.proxmox.nodes.get()
|
||||
try:
|
||||
self.permissions = self.proxmox.access.permissions.get()
|
||||
except ResourceException as err:
|
||||
if 400 <= err.status_code < 500:
|
||||
raise ProxmoxPermissionsError from err
|
||||
raise ProxmoxServerError from err
|
||||
|
||||
try:
|
||||
self.proxmox.nodes.get()
|
||||
except ResourceException as err:
|
||||
if 400 <= err.status_code < 500:
|
||||
raise ProxmoxNodesNotFoundError from err
|
||||
raise ProxmoxServerError from err
|
||||
|
||||
def _fetch_all_nodes(
|
||||
self,
|
||||
@@ -230,3 +252,19 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
|
||||
if new_containers:
|
||||
_LOGGER.debug("New containers found: %s", new_containers)
|
||||
self.known_containers.update(new_containers)
|
||||
|
||||
|
||||
class ProxmoxSetupError(Exception):
|
||||
"""Base exception for Proxmox setup issues."""
|
||||
|
||||
|
||||
class ProxmoxNodesNotFoundError(ProxmoxSetupError):
|
||||
"""Raised when the API works but no nodes are visible."""
|
||||
|
||||
|
||||
class ProxmoxPermissionsError(ProxmoxSetupError):
|
||||
"""Raised when failing to retrieve permissions."""
|
||||
|
||||
|
||||
class ProxmoxServerError(ProxmoxSetupError):
|
||||
"""Raised when the Proxmox server returns an error."""
|
||||
|
||||
13
homeassistant/components/proxmoxve/helpers.py
Normal file
13
homeassistant/components/proxmoxve/helpers.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Helpers for Proxmox VE."""
|
||||
|
||||
from .const import PERM_POWER
|
||||
|
||||
|
||||
def is_granted(
|
||||
permissions: dict[str, dict[str, int]],
|
||||
p_type: str = "vms",
|
||||
permission: str = PERM_POWER,
|
||||
) -> bool:
|
||||
"""Validate user permissions for the given type and permission."""
|
||||
path = f"/{p_type}"
|
||||
return permissions.get(path, {}).get(permission) == 1
|
||||
@@ -32,6 +32,14 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "[%key:component::proxmoxve::config::step::user::data_description::host%]",
|
||||
"password": "[%key:component::proxmoxve::config::step::user::data_description::password%]",
|
||||
"port": "[%key:component::proxmoxve::config::step::user::data_description::port%]",
|
||||
"realm": "[%key:component::proxmoxve::config::step::user::data_description::realm%]",
|
||||
"username": "[%key:component::proxmoxve::config::step::user::data_description::username%]",
|
||||
"verify_ssl": "[%key:component::proxmoxve::config::step::user::data_description::verify_ssl%]"
|
||||
},
|
||||
"description": "Use the following form to reconfigure your Proxmox VE server connection.",
|
||||
"title": "Reconfigure Proxmox VE integration"
|
||||
},
|
||||
@@ -44,6 +52,14 @@
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your Proxmox VE server",
|
||||
"password": "The password for the Proxmox VE server",
|
||||
"port": "The port of your Proxmox VE server (default: 8006)",
|
||||
"realm": "The authentication realm for the Proxmox VE server (default: 'pam')",
|
||||
"username": "The username for the Proxmox VE server",
|
||||
"verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate"
|
||||
},
|
||||
"description": "Enter your Proxmox VE server details to set up the integration.",
|
||||
"title": "Connect to Proxmox VE"
|
||||
}
|
||||
@@ -159,6 +175,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"api_error_details": {
|
||||
"message": "An error occurred while communicating with the Proxmox VE instance: {error}"
|
||||
},
|
||||
"api_error_no_details": {
|
||||
"message": "An error occurred while communicating with the Proxmox VE instance."
|
||||
},
|
||||
@@ -177,6 +196,15 @@
|
||||
"no_nodes_found": {
|
||||
"message": "No active nodes were found on the Proxmox VE server."
|
||||
},
|
||||
"no_permission_node_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
"no_permission_vm_lxc_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of VMs and containers. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
},
|
||||
"permissions_error": {
|
||||
"message": "Failed to retrieve Proxmox VE permissions. Please check your credentials and try again."
|
||||
},
|
||||
"ssl_error": {
|
||||
"message": "An SSL error occurred: {error}"
|
||||
},
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import aiohttp
|
||||
from pyrainbird.async_client import AsyncRainbirdController, CreateController
|
||||
from pyrainbird.async_client import AsyncRainbirdController, create_controller
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
|
||||
from homeassistant.const import (
|
||||
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN
|
||||
from .const import CONF_SERIAL_NUMBER, DOMAIN, TIMEOUT_SECONDS
|
||||
from .coordinator import (
|
||||
RainbirdScheduleUpdateCoordinator,
|
||||
RainbirdUpdateCoordinator,
|
||||
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
|
||||
clientsession = async_create_clientsession()
|
||||
_async_register_clientsession_shutdown(hass, entry, clientsession)
|
||||
|
||||
controller = CreateController(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(
|
||||
clientsession,
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except RainbirdAuthException as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except RainbirdApiException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
if not (await _async_fix_unique_id(hass, controller, entry)):
|
||||
return False
|
||||
|
||||
@@ -7,7 +7,7 @@ from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyrainbird.async_client import CreateController
|
||||
from pyrainbird.async_client import create_controller
|
||||
from pyrainbird.data import WifiParams
|
||||
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
|
||||
import voluptuous as vol
|
||||
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
Raises a ConfigFlowError on failure.
|
||||
"""
|
||||
clientsession = async_create_clientsession()
|
||||
controller = CreateController(clientsession, host, password)
|
||||
try:
|
||||
async with asyncio.timeout(TIMEOUT_SECONDS):
|
||||
controller = await create_controller(clientsession, host, password)
|
||||
return await asyncio.gather(
|
||||
controller.get_serial_number(),
|
||||
controller.get_wifi_params(),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Recovery Mode",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": false,
|
||||
"dependencies": ["frontend", "persistent_notification", "cloud"],
|
||||
"dependencies": ["persistent_notification"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/recovery_mode",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
|
||||
@@ -543,7 +543,20 @@ def migrate_entity_ids(
|
||||
entity.unique_id,
|
||||
new_id,
|
||||
)
|
||||
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
|
||||
existing_entity = entity_reg.async_get_entity_id(
|
||||
entity.domain, entity.platform, new_id
|
||||
)
|
||||
if existing_entity is None:
|
||||
entity_reg.async_update_entity(entity.entity_id, new_unique_id=new_id)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Reolink entity with unique_id %s already exists, "
|
||||
"removing entity with unique_id %s",
|
||||
new_id,
|
||||
entity.unique_id,
|
||||
)
|
||||
entity_reg.async_remove(entity.entity_id)
|
||||
continue
|
||||
|
||||
if entity.device_id in ch_device_ids:
|
||||
ch = ch_device_ids[entity.device_id]
|
||||
@@ -573,7 +586,7 @@ def migrate_entity_ids(
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"Reolink entity with unique_id %s already exists, "
|
||||
"removing device with unique_id %s",
|
||||
"removing entity with unique_id %s",
|
||||
new_id,
|
||||
entity.unique_id,
|
||||
)
|
||||
|
||||
@@ -159,6 +159,15 @@ class ReolinkFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle discovery via dhcp."""
|
||||
mac_address = format_mac(discovery_info.macaddress)
|
||||
existing_entry = await self.async_set_unique_id(mac_address)
|
||||
if existing_entry and CONF_HOST not in existing_entry.data:
|
||||
_LOGGER.debug(
|
||||
"Reolink DHCP discovered device with MAC '%s' and IP '%s', "
|
||||
"but existing config entry does not have host, ignoring",
|
||||
mac_address,
|
||||
discovery_info.ip,
|
||||
)
|
||||
raise AbortFlow("already_configured")
|
||||
|
||||
if (
|
||||
existing_entry
|
||||
and CONF_PASSWORD in existing_entry.data
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["reolink_aio"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["reolink-aio==0.19.0"]
|
||||
"requirements": ["reolink-aio==0.19.1"]
|
||||
}
|
||||
|
||||
@@ -31,5 +31,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["ring_doorbell"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["ring-doorbell==0.9.13"]
|
||||
"requirements": ["ring-doorbell==0.9.14"]
|
||||
}
|
||||
|
||||
@@ -188,7 +188,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = entry_data[CONF_USERNAME]
|
||||
assert self._username
|
||||
self._client = RoborockApiClient(
|
||||
self._username, session=async_get_clientsession(self.hass)
|
||||
self._username,
|
||||
base_url=entry_data[CONF_BASE_URL],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
|
||||
@@ -162,6 +162,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"missing_output_access_code": {
|
||||
"message": "Cannot control switchable outputs because no user code is configured for this Satel Integra entry. Configure a code in the integration options to enable output control."
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_yaml_import_issue_cannot_connect": {
|
||||
"description": "Configuring {integration_title} using YAML is being removed but there was an connection error importing your existing configuration.\n\nEnsure connection to {integration_title} works and restart Home Assistant to try again or remove the `{domain}` YAML configuration from your configuration.yaml file and add the {integration_title} integration manually.",
|
||||
|
||||
@@ -8,9 +8,14 @@ from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.config_entries import ConfigSubentry
|
||||
from homeassistant.const import CONF_CODE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import CONF_SWITCHABLE_OUTPUT_NUMBER, SUBENTRY_TYPE_SWITCHABLE_OUTPUT
|
||||
from .const import (
|
||||
CONF_SWITCHABLE_OUTPUT_NUMBER,
|
||||
DOMAIN,
|
||||
SUBENTRY_TYPE_SWITCHABLE_OUTPUT,
|
||||
)
|
||||
from .coordinator import SatelConfigEntry, SatelIntegraOutputsCoordinator
|
||||
from .entity import SatelIntegraEntity
|
||||
|
||||
@@ -83,12 +88,24 @@ class SatelIntegraSwitch(
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the device on."""
|
||||
if self._code is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_output_access_code",
|
||||
)
|
||||
|
||||
await self._controller.set_output(self._code, self._device_number, True)
|
||||
self._attr_is_on = True
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the device off."""
|
||||
if self._code is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="missing_output_access_code",
|
||||
)
|
||||
|
||||
await self._controller.set_output(self._code, self._device_number, False)
|
||||
self._attr_is_on = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -269,7 +269,6 @@ class LeilSaunaClimate(LeilSaunaEntity, ClimateEntity):
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="start_session_failed",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
@@ -47,5 +47,4 @@ class LeilSaunaCoordinator(DataUpdateCoordinator[SaunumData]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "Communication error: {error}"
|
||||
"message": "Communication error with sauna control unit"
|
||||
},
|
||||
"door_open": {
|
||||
"message": "Cannot start sauna session when sauna door is open"
|
||||
@@ -130,7 +130,7 @@
|
||||
"message": "Failed to set temperature to {temperature}"
|
||||
},
|
||||
"start_session_failed": {
|
||||
"message": "Failed to start sauna session: {error}"
|
||||
"message": "Failed to start sauna session"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
|
||||
@@ -124,6 +124,17 @@ class SFTPFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
}
|
||||
)
|
||||
|
||||
if not user_input[CONF_BACKUP_LOCATION].startswith("/"):
|
||||
errors[CONF_BACKUP_LOCATION] = "backup_location_relative"
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
DATA_SCHEMA, user_input
|
||||
),
|
||||
description_placeholders=placeholders,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
try:
|
||||
# Validate auth input and save uploaded key file if provided
|
||||
user_input = await self._validate_auth_and_save_keyfile(user_input)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"already_configured": "Integration already configured. Host with same address, port and backup location already exists."
|
||||
},
|
||||
"error": {
|
||||
"backup_location_relative": "The remote path must be an absolute path (starting with `/`).",
|
||||
"invalid_key": "Invalid key uploaded. Please make sure key corresponds to valid SSH key algorithm.",
|
||||
"key_or_password_needed": "Please configure password or private key file location for SFTP Storage.",
|
||||
"os_error": "{error_message}. Please check if host and/or port are correct.",
|
||||
|
||||
@@ -66,6 +66,7 @@ from .repairs import (
|
||||
from .services import async_setup_services
|
||||
from .utils import (
|
||||
async_create_issue_unsupported_firmware,
|
||||
async_migrate_rpc_sensor_description_unique_ids,
|
||||
async_migrate_rpc_virtual_components_unique_ids,
|
||||
get_coap_context,
|
||||
get_device_entry_gen,
|
||||
@@ -296,6 +297,12 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
runtime_data = entry.runtime_data
|
||||
runtime_data.platforms = RPC_SLEEPING_PLATFORMS
|
||||
|
||||
await er.async_migrate_entries(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
async_migrate_rpc_sensor_description_unique_ids,
|
||||
)
|
||||
|
||||
if sleep_period == 0:
|
||||
# Not a sleeping device, finish setup
|
||||
LOGGER.debug("Setting up online RPC device %s", entry.title)
|
||||
|
||||
@@ -1220,7 +1220,7 @@ RPC_SENSORS: Final = {
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
use_polling_coordinator=True,
|
||||
),
|
||||
"temperature_0": RpcSensorDescription(
|
||||
"temperature_tc": RpcSensorDescription(
|
||||
key="temperature",
|
||||
sub_key="tC",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
@@ -1249,7 +1249,7 @@ RPC_SENSORS: Final = {
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
use_polling_coordinator=True,
|
||||
),
|
||||
"humidity_0": RpcSensorDescription(
|
||||
"humidity_rh": RpcSensorDescription(
|
||||
key="humidity",
|
||||
sub_key="rh",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
|
||||
@@ -969,6 +969,30 @@ def format_ble_addr(ble_addr: str) -> str:
|
||||
return ble_addr.replace(":", "").upper()
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_rpc_sensor_description_unique_ids(
|
||||
entity_entry: er.RegistryEntry,
|
||||
) -> dict[str, Any] | None:
|
||||
"""Migrate RPC sensor unique_ids after sensor description key rename."""
|
||||
unique_id_map = {
|
||||
"-temperature_0": "-temperature_tc",
|
||||
"-humidity_0": "-humidity_rh",
|
||||
}
|
||||
|
||||
for old_suffix, new_suffix in unique_id_map.items():
|
||||
if entity_entry.unique_id.endswith(old_suffix):
|
||||
new_unique_id = entity_entry.unique_id.removesuffix(old_suffix) + new_suffix
|
||||
LOGGER.debug(
|
||||
"Migrating unique_id for %s entity from [%s] to [%s]",
|
||||
entity_entry.entity_id,
|
||||
entity_entry.unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
return {"new_unique_id": new_unique_id}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def async_migrate_rpc_virtual_components_unique_ids(
|
||||
config: dict[str, Any], entity_entry: er.RegistryEntry
|
||||
|
||||
@@ -34,5 +34,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.5.3"]
|
||||
"requirements": ["pysmartthings==3.6.0"]
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ ROBOT_CLEANER_TURBO_MODE_STATE_MAP = {
|
||||
|
||||
ROBOT_CLEANER_MOVEMENT_MAP = {
|
||||
"powerOff": "off",
|
||||
"washingMop": "washing_mop",
|
||||
}
|
||||
|
||||
OVEN_MODE = {
|
||||
@@ -161,6 +162,13 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription):
|
||||
use_temperature_unit: bool = False
|
||||
deprecated: Callable[[ComponentStatus], tuple[str, str] | None] | None = None
|
||||
component_translation_key: dict[str, str] | None = None
|
||||
presentation_fn: (
|
||||
Callable[
|
||||
[str | None, str | float | int | datetime | None],
|
||||
str | float | int | datetime | None,
|
||||
]
|
||||
| None
|
||||
) = None
|
||||
|
||||
|
||||
CAPABILITY_TO_SENSORS: dict[
|
||||
@@ -762,6 +770,13 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
(value := cast(dict | None, status.value)) is not None
|
||||
and "power" in value
|
||||
),
|
||||
presentation_fn=lambda presentation_id, value: (
|
||||
value * 1000
|
||||
if presentation_id is not None
|
||||
and "EHS" in presentation_id
|
||||
and isinstance(value, (int, float))
|
||||
else value
|
||||
),
|
||||
),
|
||||
SmartThingsSensorEntityDescription(
|
||||
key="deltaEnergy_meter",
|
||||
@@ -880,6 +895,7 @@ CAPABILITY_TO_SENSORS: dict[
|
||||
"after",
|
||||
"cleaning",
|
||||
"pause",
|
||||
"washing_mop",
|
||||
],
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
value_fn=lambda value: ROBOT_CLEANER_MOVEMENT_MAP.get(value, value),
|
||||
@@ -1345,7 +1361,12 @@ class SmartThingsSensor(SmartThingsEntity, SensorEntity):
|
||||
res = self.get_attribute_value(self.capability, self._attribute)
|
||||
if options_map := self.entity_description.options_map:
|
||||
return options_map.get(res)
|
||||
return self.entity_description.value_fn(res)
|
||||
value = self.entity_description.value_fn(res)
|
||||
if self.entity_description.presentation_fn:
|
||||
value = self.entity_description.presentation_fn(
|
||||
self.device.device.presentation_id, value
|
||||
)
|
||||
return value
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
|
||||
@@ -718,7 +718,8 @@
|
||||
"off": "[%key:common::state::off%]",
|
||||
"pause": "[%key:common::state::paused%]",
|
||||
"point": "Point",
|
||||
"reserve": "Reserve"
|
||||
"reserve": "Reserve",
|
||||
"washing_mop": "Washing mop"
|
||||
}
|
||||
},
|
||||
"robot_cleaner_turbo_mode": {
|
||||
|
||||
@@ -74,7 +74,7 @@ async def async_setup_entry(
|
||||
radios = coordinator.data.info.radios
|
||||
|
||||
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
|
||||
entity_created = [False, False]
|
||||
entity_created = [False] * len(radios)
|
||||
|
||||
@callback
|
||||
def _check_router(startup: bool = False) -> None:
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pysmlight==0.2.14"],
|
||||
"requirements": ["pysmlight==0.2.16"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_slzb-06._tcp.local."
|
||||
|
||||
@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
return f"{CLIENT_PREFIX}{host}_{id}"
|
||||
|
||||
@property
|
||||
def _current_group(self) -> Snapgroup:
|
||||
def _current_group(self) -> Snapgroup | None:
|
||||
"""Return the group the client is associated with."""
|
||||
return self._device.group
|
||||
|
||||
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
if self._device.connected:
|
||||
if self.is_volume_muted or self._current_group.muted:
|
||||
if (
|
||||
self.is_volume_muted
|
||||
or self._current_group is None
|
||||
or self._current_group.muted
|
||||
):
|
||||
return MediaPlayerState.IDLE
|
||||
try:
|
||||
return STREAM_STATUS.get(self._current_group.stream_status)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@property
|
||||
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Return the current input source."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
return self._current_group.stream
|
||||
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""List of available input sources."""
|
||||
if self._current_group is None:
|
||||
return []
|
||||
|
||||
return list(self._current_group.streams_by_name().keys())
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Set input source."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="select_source_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
"source": source,
|
||||
},
|
||||
)
|
||||
|
||||
streams = self._current_group.streams_by_name()
|
||||
if source in streams:
|
||||
await self._current_group.set_stream(streams[source].identifier)
|
||||
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def group_members(self) -> list[str] | None:
|
||||
"""List of player entities which are currently grouped together for synchronous playback."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
entity_registry = er.async_get(self.hass)
|
||||
return [
|
||||
entity_id
|
||||
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
|
||||
async def async_join_players(self, group_members: list[str]) -> None:
|
||||
"""Add `group_members` to this client's current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="join_players_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Get the client entity for each group member excluding self
|
||||
entity_registry = er.async_get(self.hass)
|
||||
clients = [
|
||||
@@ -271,13 +304,25 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_unjoin_player(self) -> None:
|
||||
"""Remove this client from it's current group."""
|
||||
"""Remove this client from its current group."""
|
||||
if self._current_group is None:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unjoin_no_group",
|
||||
translation_placeholders={
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
)
|
||||
|
||||
await self._current_group.remove_client(self._device.identifier)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def metadata(self) -> Mapping[str, Any]:
|
||||
"""Get metadata from the current stream."""
|
||||
if self._current_group is None:
|
||||
return {}
|
||||
|
||||
try:
|
||||
if metadata := self.coordinator.server.stream(
|
||||
self._current_group.stream
|
||||
@@ -341,6 +386,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of current playing media in seconds."""
|
||||
if self._current_group is None:
|
||||
return None
|
||||
|
||||
try:
|
||||
# Position is part of properties object, not metadata object
|
||||
if properties := self.coordinator.server.stream(
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"join_players_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to join players."
|
||||
},
|
||||
"select_source_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to select source {source}."
|
||||
},
|
||||
"unjoin_no_group": {
|
||||
"message": "Client {entity_id} has no group. Unable to unjoin player."
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"restore": {
|
||||
"description": "Restores a previously taken snapshot of a media player.",
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import fields
|
||||
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.sonarr_client import SonarrClient
|
||||
|
||||
@@ -37,7 +39,6 @@ from .coordinator import (
|
||||
SeriesDataUpdateCoordinator,
|
||||
SonarrConfigEntry,
|
||||
SonarrData,
|
||||
SonarrDataUpdateCoordinator,
|
||||
StatusDataUpdateCoordinator,
|
||||
WantedDataUpdateCoordinator,
|
||||
)
|
||||
@@ -89,16 +90,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: SonarrConfigEntry) -> bo
|
||||
)
|
||||
# Temporary, until we add diagnostic entities
|
||||
_version = None
|
||||
coordinators: list[SonarrDataUpdateCoordinator] = [
|
||||
data.upcoming,
|
||||
data.commands,
|
||||
data.diskspace,
|
||||
data.queue,
|
||||
data.series,
|
||||
data.status,
|
||||
data.wanted,
|
||||
]
|
||||
for coordinator in coordinators:
|
||||
for field in fields(data):
|
||||
coordinator = getattr(data, field.name)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
if isinstance(coordinator, StatusDataUpdateCoordinator):
|
||||
_version = coordinator.data.version
|
||||
|
||||
@@ -128,35 +128,6 @@ def format_queue(
|
||||
return shows
|
||||
|
||||
|
||||
def format_episode_item(
|
||||
series: SonarrSeries, episode_data: dict[str, Any], base_url: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Format a single episode item."""
|
||||
result: dict[str, Any] = {
|
||||
"id": episode_data.get("id"),
|
||||
"episode_number": episode_data.get("episodeNumber"),
|
||||
"season_number": episode_data.get("seasonNumber"),
|
||||
"title": episode_data.get("title"),
|
||||
"air_date": str(episode_data.get("airDate", "")),
|
||||
"overview": episode_data.get("overview"),
|
||||
"has_file": episode_data.get("hasFile", False),
|
||||
"monitored": episode_data.get("monitored", False),
|
||||
}
|
||||
|
||||
# Add episode images if available
|
||||
if images := episode_data.get("images"):
|
||||
result["images"] = {}
|
||||
for image in images:
|
||||
cover_type = image.coverType
|
||||
# Prefer remoteUrl (public TVDB URL) over local path
|
||||
if remote_url := getattr(image, "remoteUrl", None):
|
||||
result["images"][cover_type] = remote_url
|
||||
elif base_url and (url := getattr(image, "url", None)):
|
||||
result["images"][cover_type] = f"{base_url.rstrip('/')}{url}"
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def format_series(
|
||||
series_list: list[SonarrSeries], base_url: str | None = None
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user