mirror of
https://github.com/home-assistant/core.git
synced 2026-03-21 10:14:52 +01:00
Compare commits
171 Commits
add_text_c
...
rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3c27e9f93 | ||
|
|
92e237ade2 | ||
|
|
cbc573a6b1 | ||
|
|
0c059cfc27 | ||
|
|
143ce9d7b3 | ||
|
|
a6aa837d40 | ||
|
|
c58b4a0066 | ||
|
|
5155242ba7 | ||
|
|
085680f6bf | ||
|
|
98ecaaa6d2 | ||
|
|
5ad199fe16 | ||
|
|
413cb98424 | ||
|
|
b38c5bcaf2 | ||
|
|
fa85dfb3b5 | ||
|
|
f0c6a035db | ||
|
|
3f0c200e56 | ||
|
|
a2259ede28 | ||
|
|
24c2b6fe81 | ||
|
|
efc7350e6f | ||
|
|
5f525fc2a1 | ||
|
|
f619a3e7af | ||
|
|
4e43492342 | ||
|
|
39e70071d3 | ||
|
|
6da0936a66 | ||
|
|
5257702530 | ||
|
|
93da5be052 | ||
|
|
e3c6a2184d | ||
|
|
0ba0829350 | ||
|
|
678048e681 | ||
|
|
743eeeae53 | ||
|
|
46555c6d9a | ||
|
|
dbaca0a723 | ||
|
|
9bb2959029 | ||
|
|
0304781fa9 | ||
|
|
e081d28aa4 | ||
|
|
34aa28c72f | ||
|
|
cfa2946db8 | ||
|
|
1b0779347c | ||
|
|
93a281e7af | ||
|
|
6b32e27fd3 | ||
|
|
79928a8c7c | ||
|
|
9146518e13 | ||
|
|
e9c5172f43 | ||
|
|
cce21ad4b9 | ||
|
|
10ec02ca3c | ||
|
|
bdf54491e5 | ||
|
|
0b05d34238 | ||
|
|
4c69a1c5f7 | ||
|
|
6f1f56dcaa | ||
|
|
d0b9991232 | ||
|
|
aacf39be8a | ||
|
|
bf055da82c | ||
|
|
0fb118bcd9 | ||
|
|
954ef7d1f5 | ||
|
|
b091299320 | ||
|
|
52483e18b2 | ||
|
|
57e8683ed7 | ||
|
|
67faace978 | ||
|
|
e4be64fcb1 | ||
|
|
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 | ||
|
|
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,5 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
|
||||
from aioamazondevices.const.devices import SPEAKER_GROUP_MODEL
|
||||
from aioamazondevices.structures import AmazonDevice
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
@@ -25,19 +24,15 @@ 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")
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_num)},
|
||||
name=self.device.account_name,
|
||||
model=model,
|
||||
model=self.device.model,
|
||||
model_id=self.device.device_type,
|
||||
manufacturer=model_details.get("manufacturer", "Amazon"),
|
||||
hw_version=model_details.get("hw_version"),
|
||||
sw_version=(
|
||||
self.device.software_version if model != SPEAKER_GROUP_MODEL else None
|
||||
),
|
||||
serial_number=serial_num if model != SPEAKER_GROUP_MODEL else None,
|
||||
manufacturer=self.device.manufacturer or "Amazon",
|
||||
hw_version=self.device.hardware_version,
|
||||
sw_version=self.device.software_version,
|
||||
serial_number=serial_num,
|
||||
)
|
||||
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.1"]
|
||||
}
|
||||
|
||||
@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
|
||||
assert method is not None
|
||||
|
||||
await method(self.device, state)
|
||||
await self.coordinator.async_request_refresh()
|
||||
self.coordinator.data[self.device.serial_number].sensors[
|
||||
self.entity_description.key
|
||||
].value = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyanglianwater"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyanglianwater==3.1.0"]
|
||||
"requirements": ["pyanglianwater==3.1.1"]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from aiohttp import ClientError
|
||||
from yalexs.exceptions import AugustApiAIOHTTPError
|
||||
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
|
||||
from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
ConfigEntryNotReady,
|
||||
OAuth2TokenRequestError,
|
||||
OAuth2TokenRequestReauthError,
|
||||
)
|
||||
from homeassistant.helpers import device_registry as dr, issue_registry as ir
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
|
||||
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
|
||||
try:
|
||||
await async_setup_august(hass, entry, august_gateway)
|
||||
except OAuth2TokenRequestReauthError as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except (RequireValidation, InvalidAuth) as err:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
except TimeoutError as err:
|
||||
raise ConfigEntryNotReady("Timed out connecting to august api") from err
|
||||
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
|
||||
except (
|
||||
AugustApiAIOHTTPError,
|
||||
OAuth2TokenRequestError,
|
||||
ClientError,
|
||||
CannotConnect,
|
||||
) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return 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.8"]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==66"],
|
||||
"requirements": ["axis==67"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -246,6 +246,8 @@ def decrypt_backup(
|
||||
except (DecryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error decrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
error = err
|
||||
@@ -332,8 +334,10 @@ def encrypt_backup(
|
||||
except (EncryptError, SecureTarError, tarfile.TarError) as err:
|
||||
LOGGER.warning("Error encrypting backup: %s", err)
|
||||
error = err
|
||||
except Abort:
|
||||
raise
|
||||
except Exception as err: # noqa: BLE001
|
||||
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
|
||||
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
|
||||
error = err
|
||||
else:
|
||||
# Pad the output stream to the requested minimum size
|
||||
|
||||
@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_PASSKEY, DOMAIN
|
||||
from .const import CONF_PASSKEY, DOMAIN, LOGGER
|
||||
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
@@ -52,7 +52,7 @@ class BSBLanData:
|
||||
client: BSBLAN
|
||||
device: Device
|
||||
info: Info
|
||||
static: StaticState
|
||||
static: StaticState | None
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
# the connection by fetching firmware version
|
||||
await bsblan.initialize()
|
||||
|
||||
# Fetch device metadata in parallel for faster startup
|
||||
device, info, static = await asyncio.gather(
|
||||
# Fetch required device metadata in parallel for faster startup
|
||||
device, info = await asyncio.gather(
|
||||
bsblan.device(),
|
||||
bsblan.info(),
|
||||
bsblan.static_values(),
|
||||
)
|
||||
except BSBLANConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
|
||||
translation_key="setup_general_error",
|
||||
) from err
|
||||
|
||||
try:
|
||||
static = await bsblan.static_values()
|
||||
except (BSBLANError, TimeoutError) as err:
|
||||
LOGGER.debug(
|
||||
"Static values not available for %s: %s",
|
||||
entry.data[CONF_HOST],
|
||||
err,
|
||||
)
|
||||
static = None
|
||||
|
||||
# Create coordinators with the already-initialized client
|
||||
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
|
||||
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
|
||||
|
||||
@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
|
||||
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
|
||||
|
||||
# Set temperature range if available, otherwise use Home Assistant defaults
|
||||
if data.static.min_temp is not None and data.static.min_temp.value is not None:
|
||||
self._attr_min_temp = data.static.min_temp.value
|
||||
if data.static.max_temp is not None and data.static.max_temp.value is not None:
|
||||
self._attr_max_temp = data.static.max_temp.value
|
||||
if (static := data.static) is not None:
|
||||
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
|
||||
self._attr_min_temp = min_temp.value
|
||||
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
|
||||
self._attr_max_temp = max_temp.value
|
||||
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
|
||||
|
||||
@property
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
},
|
||||
"static": data.static.model_dump(),
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
# Add DHW config and schedule from slow coordinator if available
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["casttube", "pychromecast"],
|
||||
"requirements": ["PyChromecast==14.0.9"],
|
||||
"requirements": ["PyChromecast==14.0.10"],
|
||||
"single_config_entry": true,
|
||||
"zeroconf": ["_googlecast._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.0"]
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._use_tls = user_input[CONF_SSL]
|
||||
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||
|
||||
self._port = self._determine_port(user_input)
|
||||
|
||||
|
||||
@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
|
||||
]
|
||||
)
|
||||
_LOGGER.debug("WiFi networks count: %s", wifi_count)
|
||||
networks: dict = {}
|
||||
networks: dict[int, dict[str, Any]] = {}
|
||||
for i in range(1, wifi_count + 1):
|
||||
network_info = await avm_wrapper.async_get_wlan_configuration(i)
|
||||
# Devices with 4 WLAN services, use the 2nd for internal communications
|
||||
if not (wifi_count == 4 and i == 2):
|
||||
networks[i] = {
|
||||
"ssid": network_info["NewSSID"],
|
||||
"bssid": network_info["NewBSSID"],
|
||||
"standard": network_info["NewStandard"],
|
||||
"enabled": network_info["NewEnable"],
|
||||
"status": network_info["NewStatus"],
|
||||
}
|
||||
networks[i] = network_info
|
||||
for i, network in networks.copy().items():
|
||||
networks[i]["switch_name"] = network["ssid"]
|
||||
networks[i]["switch_name"] = network["NewSSID"]
|
||||
if (
|
||||
len(
|
||||
[
|
||||
j
|
||||
for j, n in networks.items()
|
||||
if slugify(n["ssid"]) == slugify(network["ssid"])
|
||||
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
|
||||
]
|
||||
)
|
||||
> 1
|
||||
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
|
||||
for key, attr in attributes_dict.items():
|
||||
self._attributes[attr] = self.port_mapping[key]
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
|
||||
|
||||
resp = await self._avm_wrapper.async_add_port_mapping(
|
||||
await self._avm_wrapper.async_add_port_mapping(
|
||||
self.connection_type, self.port_mapping
|
||||
)
|
||||
return bool(resp is not None)
|
||||
|
||||
|
||||
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||
"""Turn off switch."""
|
||||
await self._async_handle_turn_on_off(turn_on=False)
|
||||
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
|
||||
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||
"""Handle switch state change request."""
|
||||
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
|
||||
self._avm_wrapper.devices[self._mac].wan_access = turn_on
|
||||
self.async_write_ha_state()
|
||||
return True
|
||||
|
||||
|
||||
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
avm_wrapper: AvmWrapper,
|
||||
device_friendly_name: str,
|
||||
network_num: int,
|
||||
network_data: dict,
|
||||
network_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Init Fritz Wifi switch."""
|
||||
self._avm_wrapper = avm_wrapper
|
||||
self._wifi_info = network_data
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
type=SWITCH_TYPE_WIFINETWORK,
|
||||
callback_update=self._async_fetch_update,
|
||||
callback_switch=self._async_switch_on_off_executor,
|
||||
init_state=network_data["enabled"],
|
||||
init_state=network_data["NewEnable"],
|
||||
)
|
||||
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
|
||||
|
||||
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
self._attributes["mac_address_control"] = wifi_info[
|
||||
"NewMACAddressControlEnabled"
|
||||
]
|
||||
self._wifi_info = wifi_info
|
||||
|
||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
||||
"""Handle wifi switch."""
|
||||
self._wifi_info["NewEnable"] = turn_on
|
||||
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260225.0"]
|
||||
"requirements": ["home-assistant-frontend==20260312.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"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
|
||||
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["network"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-local-api==2.3.0"]
|
||||
"requirements": ["govee-local-api==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -117,13 +117,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
|
||||
# Map raw event type names to friendly names using SENSOR_MAP
|
||||
mapped_events: dict[str, list[int]] = {}
|
||||
for event_type, channels in nvr_events.items():
|
||||
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
|
||||
event_key = event_type.lower()
|
||||
# Skip videoloss - used as watchdog by pyhik, not a real sensor
|
||||
if event_key == "videoloss":
|
||||
continue
|
||||
friendly_name = SENSOR_MAP.get(event_key)
|
||||
if friendly_name is None:
|
||||
_LOGGER.debug("Skipping unmapped event type: %s", event_type)
|
||||
continue
|
||||
if friendly_name in mapped_events:
|
||||
mapped_events[friendly_name].extend(channels)
|
||||
else:
|
||||
mapped_events[friendly_name] = list(channels)
|
||||
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
|
||||
camera.inject_events(mapped_events)
|
||||
if mapped_events:
|
||||
camera.inject_events(mapped_events)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"No event triggers returned from %s. "
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from apyhiveapi import Auth
|
||||
@@ -26,6 +27,8 @@ from homeassistant.core import callback
|
||||
from . import HiveConfigEntry
|
||||
from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a Hive config flow."""
|
||||
@@ -36,7 +39,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
self.tokens: dict[str, str] = {}
|
||||
self.tokens: dict[str, Any] = {}
|
||||
self.device_registration: bool = False
|
||||
self.device_name = "Home Assistant"
|
||||
|
||||
@@ -67,11 +70,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
except HiveApiError:
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if (
|
||||
auth_result := self.tokens.get("AuthenticationResult", {})
|
||||
) and auth_result.get("NewDeviceMetadata"):
|
||||
_LOGGER.debug("Login successful, New device detected")
|
||||
self.device_registration = True
|
||||
return await self.async_step_configuration()
|
||||
|
||||
if self.tokens.get("ChallengeName") == "SMS_MFA":
|
||||
_LOGGER.debug("Login successful, SMS 2FA required")
|
||||
# Complete SMS 2FA.
|
||||
return await self.async_step_2fa()
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug(
|
||||
"Login successful, no new device detected, no 2FA required"
|
||||
)
|
||||
# Complete the entry.
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
@@ -103,6 +117,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "no_internet_available"
|
||||
|
||||
if not errors:
|
||||
_LOGGER.debug("2FA successful")
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return await self.async_setup_hive_entry()
|
||||
self.device_registration = True
|
||||
@@ -119,10 +134,11 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
if user_input:
|
||||
if self.device_registration:
|
||||
_LOGGER.debug("Attempting to register device")
|
||||
self.device_name = user_input["device_name"]
|
||||
await self.hive_auth.device_registration(user_input["device_name"])
|
||||
self.data["device_data"] = await self.hive_auth.get_device_data()
|
||||
|
||||
_LOGGER.debug("Device registration successful")
|
||||
try:
|
||||
return await self.async_setup_hive_entry()
|
||||
except UnknownHiveError:
|
||||
@@ -142,6 +158,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
raise UnknownHiveError
|
||||
|
||||
# Setup the config entry
|
||||
_LOGGER.debug("Setting up Hive entry")
|
||||
self.data["tokens"] = self.tokens
|
||||
if self.source == SOURCE_REAUTH:
|
||||
return self.async_update_reload_and_abort(
|
||||
@@ -160,6 +177,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
CONF_USERNAME: entry_data[CONF_USERNAME],
|
||||
CONF_PASSWORD: entry_data[CONF_PASSWORD],
|
||||
}
|
||||
_LOGGER.debug("Reauthenticating user")
|
||||
return await self.async_step_user(data)
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -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,),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["hyponcloud==0.3.0"]
|
||||
"requirements": ["hyponcloud==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -21,11 +21,17 @@ from .coordinator import HypontechConfigEntry, HypontechDataCoordinator
|
||||
from .entity import HypontechEntity, HypontechPlantEntity
|
||||
|
||||
|
||||
def _power_unit(data: OverviewData | PlantData) -> str:
|
||||
"""Return the unit of measurement for power based on the API unit."""
|
||||
return UnitOfPower.KILO_WATT if data.company.upper() == "KW" else UnitOfPower.WATT
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HypontechSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech overview sensor entity."""
|
||||
|
||||
value_fn: Callable[[OverviewData], float | None]
|
||||
unit_fn: Callable[[OverviewData], str] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -33,15 +39,16 @@ class HypontechPlantSensorDescription(SensorEntityDescription):
|
||||
"""Describes Hypontech plant sensor entity."""
|
||||
|
||||
value_fn: Callable[[PlantData], float | None]
|
||||
unit_fn: Callable[[PlantData], str] | None = None
|
||||
|
||||
|
||||
OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
HypontechSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -64,10 +71,10 @@ OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
|
||||
PLANT_SENSORS: tuple[HypontechPlantSensorDescription, ...] = (
|
||||
HypontechPlantSensorDescription(
|
||||
key="pv_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda c: c.power,
|
||||
unit_fn=_power_unit,
|
||||
),
|
||||
HypontechPlantSensorDescription(
|
||||
key="lifetime_energy",
|
||||
@@ -124,6 +131,13 @@ class HypontechOverviewSensor(HypontechEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.account_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.coordinator.data.overview)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -146,6 +160,13 @@ class HypontechPlantSensor(HypontechPlantEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{plant_id}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self.entity_description.unit_fn is not None:
|
||||
return self.entity_description.unit_fn(self.plant)
|
||||
return super().native_unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -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%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
|
||||
IntellifireSensorEntityDescription(
|
||||
key="timer_end_timestamp",
|
||||
translation_key="timer_end_timestamp",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
value_fn=_time_remaining_to_timestamp,
|
||||
),
|
||||
|
||||
@@ -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 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.1"]
|
||||
"requirements": ["pyjvcprojector==2.0.3"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
"""Representation of an LG soundbar device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_state = MediaPlayerState.ON
|
||||
_attr_state = MediaPlayerState.OFF
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
|
||||
self._treble = 0
|
||||
self._device = None
|
||||
self._support_play_control = False
|
||||
self._device_on = False
|
||||
self._stream_type = 0
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)}, name=host
|
||||
)
|
||||
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
if "i_curr_func" in data:
|
||||
self._function = data["i_curr_func"]
|
||||
if "b_powerstatus" in data:
|
||||
self._device_on = data["b_powerstatus"]
|
||||
if data["b_powerstatus"]:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
|
||||
|
||||
def _update_playinfo(self, data: dict[str, Any]) -> None:
|
||||
"""Update the player info."""
|
||||
if "i_stream_type" in data:
|
||||
if self._stream_type != data["i_stream_type"]:
|
||||
self._stream_type = data["i_stream_type"]
|
||||
# Ask device for current play info when stream type changed.
|
||||
self._device.get_play()
|
||||
if data["i_stream_type"] == 0:
|
||||
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
|
||||
# the last track info should be cleared and the state should only be on or off,
|
||||
# as all playing/paused are not applicable in this mode
|
||||
self._attr_media_image_url = None
|
||||
self._attr_media_artist = None
|
||||
self._attr_media_title = None
|
||||
if self._device_on:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
if "i_play_ctrl" in data:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if self._device_on and self._stream_type != 0:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if "s_albumart" in data:
|
||||
self._attr_media_image_url = data["s_albumart"]
|
||||
self._attr_media_image_url = data["s_albumart"].strip() or None
|
||||
if "s_artist" in data:
|
||||
self._attr_media_artist = data["s_artist"]
|
||||
self._attr_media_artist = data["s_artist"].strip() or None
|
||||
if "s_title" in data:
|
||||
self._attr_media_title = data["s_title"]
|
||||
self._attr_media_title = data["s_title"].strip() or None
|
||||
if "b_support_play_ctrl" in data:
|
||||
self._support_play_control = data["b_support_play_ctrl"]
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==13.2.0"]
|
||||
"requirements": ["ical==13.2.2"]
|
||||
}
|
||||
|
||||
@@ -80,6 +80,7 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
# Matter server.
|
||||
_attr_should_poll = True
|
||||
_software_update: MatterSoftwareVersion | None = None
|
||||
_installed_software_version: int | None = None
|
||||
_cancel_update: CALLBACK_TYPE | None = None
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL
|
||||
@@ -92,6 +93,9 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
def _update_from_device(self) -> None:
|
||||
"""Update from device."""
|
||||
|
||||
self._installed_software_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersion
|
||||
)
|
||||
self._attr_installed_version = self.get_matter_attribute_value(
|
||||
clusters.BasicInformation.Attributes.SoftwareVersionString
|
||||
)
|
||||
@@ -123,6 +127,22 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
else:
|
||||
self._attr_update_percentage = None
|
||||
|
||||
def _format_latest_version(
|
||||
self, update_information: MatterSoftwareVersion
|
||||
) -> str | None:
|
||||
"""Return the version string to expose in Home Assistant."""
|
||||
latest_version = update_information.software_version_string
|
||||
if self._installed_software_version is None:
|
||||
return latest_version
|
||||
|
||||
if update_information.software_version == self._installed_software_version:
|
||||
return self._attr_installed_version or latest_version
|
||||
|
||||
if latest_version == self._attr_installed_version:
|
||||
return f"{latest_version} ({update_information.software_version})"
|
||||
|
||||
return latest_version
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Call when the entity needs to be updated."""
|
||||
try:
|
||||
@@ -130,11 +150,13 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
node_id=self._endpoint.node.node_id
|
||||
)
|
||||
if not update_information:
|
||||
self._software_update = None
|
||||
self._attr_latest_version = self._attr_installed_version
|
||||
self._attr_release_url = None
|
||||
return
|
||||
|
||||
self._software_update = update_information
|
||||
self._attr_latest_version = update_information.software_version_string
|
||||
self._attr_latest_version = self._format_latest_version(update_information)
|
||||
self._attr_release_url = update_information.release_notes_url
|
||||
|
||||
except UpdateCheckError as err:
|
||||
@@ -212,7 +234,12 @@ class MatterUpdate(MatterEntity, UpdateEntity):
|
||||
|
||||
software_version: str | int | None = version
|
||||
if self._software_update is not None and (
|
||||
version is None or version == self._software_update.software_version_string
|
||||
version is None
|
||||
or version
|
||||
in {
|
||||
self._software_update.software_version_string,
|
||||
self._attr_latest_version,
|
||||
}
|
||||
):
|
||||
# Update to the version previously fetched and shown.
|
||||
# We can pass the integer version directly to speedup download.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
|
||||
finished = 522, 11012
|
||||
extra_dry = 523
|
||||
hand_iron = 524
|
||||
hygiene_drying = 525
|
||||
moisten = 526
|
||||
thermo_spin = 527
|
||||
timed_drying = 528
|
||||
@@ -617,11 +618,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 +669,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 +737,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 +899,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 +1144,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 +1264,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",
|
||||
@@ -1005,6 +1006,7 @@
|
||||
"heating_up_phase": "Heating up phase",
|
||||
"hot_milk": "Hot milk",
|
||||
"hygiene": "Hygiene",
|
||||
"hygiene_drying": "Hygiene drying",
|
||||
"interim_rinse": "Interim rinse",
|
||||
"keep_warm": "Keep warm",
|
||||
"keeping_warm": "Keeping warm",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
|
||||
latitude: float | None
|
||||
longitude: float | None
|
||||
gps_accuracy: float
|
||||
# Reset manually set location to allow automatic zone detection
|
||||
self._attr_location_name = None
|
||||
if isinstance(
|
||||
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
|
||||
) and isinstance(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
|
||||
return {
|
||||
"device_info": client.device_info,
|
||||
"vehicles": client.vehicles,
|
||||
"ct_connected": client.ct_connected,
|
||||
"cap_available": client.cap_available,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["ohme==1.6.0"]
|
||||
"requirements": ["ohme==1.7.0"]
|
||||
}
|
||||
|
||||
@@ -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.7"]
|
||||
}
|
||||
|
||||
@@ -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.7"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["opower==0.17.0"]
|
||||
"requirements": ["opower==0.17.1"]
|
||||
}
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/otbr",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["python-otbr-api==2.8.0"]
|
||||
"requirements": ["python-otbr-api==2.9.0"]
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
|
||||
self._abort_if_unique_id_configured()
|
||||
# Logic that can be reverted back once the new unique ID is in
|
||||
existing_entry = await self.async_set_unique_id(
|
||||
user_input[CONF_API_TOKEN]
|
||||
)
|
||||
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
|
||||
return self.async_abort(reason="already_configured")
|
||||
return self.async_update_reload_and_abort(
|
||||
reconf_entry,
|
||||
data_updates={
|
||||
|
||||
@@ -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.33"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -103,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -146,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
ProxmoxContainerButtonEntityDescription(
|
||||
key="restart",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
|
||||
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
device_class=ButtonDeviceClass.RESTART,
|
||||
@@ -276,10 +277,16 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the node button action via executor."""
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
|
||||
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,
|
||||
self._node_data.node["node"],
|
||||
node_id,
|
||||
)
|
||||
|
||||
|
||||
@@ -303,11 +310,17 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the VM button action via executor."""
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
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,
|
||||
self._node_name,
|
||||
self.vm_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
|
||||
@@ -331,9 +344,16 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
|
||||
async def _async_press_call(self) -> None:
|
||||
"""Execute the container button action via executor."""
|
||||
vmid = self.container_data["vmid"]
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
|
||||
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,
|
||||
self._node_name,
|
||||
self.container_data["vmid"],
|
||||
vmid,
|
||||
)
|
||||
|
||||
@@ -17,3 +17,5 @@ DEFAULT_VERIFY_SSL = True
|
||||
TYPE_VM = 0
|
||||
TYPE_CONTAINER = 1
|
||||
UPDATE_INTERVAL = 60
|
||||
|
||||
PERM_POWER = "VM.PowerMgmt"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user