mirror of
https://github.com/home-assistant/core.git
synced 2026-05-06 16:47:03 +02:00
Compare commits
132 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fec38ef28 | |||
| 9c4b6951ef | |||
| 7d2f303035 | |||
| c61c09fba3 | |||
| 83c807d01c | |||
| 1b0386ddfc | |||
| 0af6a85049 | |||
| 7bd0bc9c8a | |||
| b200930fd4 | |||
| 5c046a3750 | |||
| c1a013d718 | |||
| f1f6cdae2a | |||
| f98de4618a | |||
| 6b2033b060 | |||
| d9dc2bbae4 | |||
| 82a1884085 | |||
| c46f6721bc | |||
| ed3ff38d30 | |||
| fd3e12a85f | |||
| 1e0a0b70f4 | |||
| 2598dde7aa | |||
| 9af7fe22bd | |||
| 546eef2eee | |||
| d65b7ce2f3 | |||
| b09671a409 | |||
| 412771465d | |||
| 3a72bc23b9 | |||
| b3d7ba5ce5 | |||
| 4e7b6838eb | |||
| 437f5ef66c | |||
| f886b03e14 | |||
| 190ee49e3a | |||
| f7c5a51f46 | |||
| e4e9c22016 | |||
| f2df848e3f | |||
| cdce98faaf | |||
| fde103cdfd | |||
| fcd6c6e335 | |||
| 8f2cec26e3 | |||
| 05463cde99 | |||
| a948799a6e | |||
| 624fab064a | |||
| a331cb7199 | |||
| 7d6eaf40a6 | |||
| 1ae9e7c87d | |||
| 6bcfc32d48 | |||
| 0b5f85bdb9 | |||
| d153eee822 | |||
| afcc2113ce | |||
| ae5bd63993 | |||
| 78107c478d | |||
| 84490ef0bb | |||
| 887e14638b | |||
| 818bde1d5e | |||
| 83da18b761 | |||
| bd904caea1 | |||
| 500f030eaa | |||
| ce755f5f8f | |||
| fb766d164b | |||
| 394670e33f | |||
| f79285f9ab | |||
| a422611ada | |||
| 4c34dcd560 | |||
| 1aca993c12 | |||
| a8cc099b66 | |||
| c56d67c02f | |||
| 0ce98cfb34 | |||
| 4a13ab9aff | |||
| dc65646d8b | |||
| 39fbdad775 | |||
| b4f6a43a14 | |||
| e5ff7a9944 | |||
| ca9945f750 | |||
| b028e2a6ae | |||
| 6f4aca495b | |||
| a892b5364d | |||
| f57e682a98 | |||
| 3493517b6d | |||
| b5842b8484 | |||
| 3333b8d019 | |||
| 745860553c | |||
| 7188a09a59 | |||
| 96a9b89412 | |||
| 586d7ab526 | |||
| 5f2fe4ffd4 | |||
| 040192c103 | |||
| e85430105e | |||
| 5c7c0a6e83 | |||
| c7bd673d01 | |||
| 6d3a93df81 | |||
| 6a934b5fe3 | |||
| d644348dc8 | |||
| dbfde9266c | |||
| ed0b68ec4a | |||
| c32d523f63 | |||
| 98a4e27e35 | |||
| fb1365e9a4 | |||
| 850b034a5f | |||
| b880876e0e | |||
| ab601e5717 | |||
| 7eda592c72 | |||
| b981ece163 | |||
| 7ea931fdc8 | |||
| f3038a20af | |||
| de234c7190 | |||
| 399681984f | |||
| 5ca14ca7d7 | |||
| ac53cfa85a | |||
| 02f1a9c3a9 | |||
| f93fdceac9 | |||
| 711a89f7b8 | |||
| 19e58c554e | |||
| feb6c2bfe6 | |||
| 6bb91422ff | |||
| 3bd699285b | |||
| 6d10305197 | |||
| 42a9c8488d | |||
| c6c273559e | |||
| f7394ce302 | |||
| 175dec6f1a | |||
| d137761cb5 | |||
| 8055cbc58d | |||
| c9dff27590 | |||
| c913a858b6 | |||
| 4ed33a804e | |||
| 8bf5674826 | |||
| b8a0b0083b | |||
| a57c101b5e | |||
| 957b8c1c52 | |||
| bb002d051b | |||
| 2b2fd4ac92 | |||
| f4c270629b |
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Get information
|
||||
id: info
|
||||
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
|
||||
uses: home-assistant/actions/helpers/info@5f5b077d63a1e4c53019231409a0c4d791fb74e5 # zizmor: ignore[unpinned-uses]
|
||||
|
||||
- name: Get version
|
||||
id: version
|
||||
|
||||
Generated
+1
-1
@@ -29,7 +29,7 @@ RUN \
|
||||
# Verify go2rtc can be executed
|
||||
go2rtc --version \
|
||||
# Install uv
|
||||
&& pip3 install uv==0.11.1
|
||||
&& pip3 install uv==0.11.6
|
||||
|
||||
WORKDIR /usr/src
|
||||
|
||||
|
||||
@@ -54,7 +54,16 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
entry.data[CONF_PASSWORD],
|
||||
entry.data[CONF_LOGIN_DATA],
|
||||
)
|
||||
self.previous_devices: set[str] = set()
|
||||
device_registry = dr.async_get(hass)
|
||||
self.previous_devices: set[str] = {
|
||||
identifier
|
||||
for device in device_registry.devices.get_devices_for_config_entry_id(
|
||||
entry.entry_id
|
||||
)
|
||||
if device.entry_type != dr.DeviceEntryType.SERVICE
|
||||
for identifier_domain, identifier in device.identifiers
|
||||
if identifier_domain == DOMAIN
|
||||
}
|
||||
|
||||
async def _async_update_data(self) -> dict[str, AmazonDevice]:
|
||||
"""Update device data."""
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioamazondevices==13.3.2"]
|
||||
"requirements": ["aioamazondevices==13.4.1"]
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
_LOGGER.debug("Updating statistics for the first time")
|
||||
usage_sum = 0.0
|
||||
last_stats_time = None
|
||||
allow_update_last_stored_hour = False
|
||||
else:
|
||||
if not meter.readings or len(meter.readings) == 0:
|
||||
_LOGGER.debug("No recent usage statistics found, skipping update")
|
||||
@@ -107,6 +108,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
_LOGGER.debug("Getting statistics at %s", start)
|
||||
stats: dict[str, list[Any]] = {}
|
||||
for end in (start + timedelta(seconds=1), None):
|
||||
stats = await get_instance(self.hass).async_add_executor_job(
|
||||
statistics_during_period,
|
||||
@@ -127,15 +129,28 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
"Not found, trying to find oldest statistic after %s",
|
||||
start,
|
||||
)
|
||||
assert stats
|
||||
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
if not stats or not stats.get(usage_statistic_id):
|
||||
_LOGGER.debug(
|
||||
"Could not find existing statistics during period lookup for %s, "
|
||||
"falling back to last stored statistic",
|
||||
usage_statistic_id,
|
||||
)
|
||||
allow_update_last_stored_hour = True
|
||||
last_records = last_stat[usage_statistic_id]
|
||||
usage_sum = float(last_records[0].get("sum") or 0.0)
|
||||
last_stats_time = last_records[0]["start"]
|
||||
else:
|
||||
allow_update_last_stored_hour = False
|
||||
records = stats[usage_statistic_id]
|
||||
|
||||
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
|
||||
last_stats_time = stats[usage_statistic_id][0]["start"]
|
||||
def _safe_get_sum(records: list[Any]) -> float:
|
||||
if records and "sum" in records[0]:
|
||||
return float(records[0]["sum"])
|
||||
return 0.0
|
||||
|
||||
usage_sum = _safe_get_sum(records)
|
||||
last_stats_time = records[0]["start"]
|
||||
|
||||
usage_statistics = []
|
||||
|
||||
@@ -148,7 +163,13 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
|
||||
)
|
||||
continue
|
||||
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
|
||||
if last_stats_time is not None and start.timestamp() <= last_stats_time:
|
||||
if last_stats_time is not None and (
|
||||
start.timestamp() < last_stats_time
|
||||
or (
|
||||
start.timestamp() == last_stats_time
|
||||
and not allow_update_last_stored_hour
|
||||
)
|
||||
):
|
||||
continue
|
||||
usage_state = max(0, read["consumption"] / 1000)
|
||||
usage_sum = max(0, read["read"])
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.8.2"],
|
||||
"requirements": ["arcam-fmj==1.8.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
|
||||
value_fn=lambda state: (
|
||||
vp.colorspace.name.lower()
|
||||
if (vp := state.get_incoming_video_parameters()) is not None
|
||||
and vp.colorspace is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
},
|
||||
"services": {
|
||||
"announce": {
|
||||
"description": "Lets a satellite announce a message.",
|
||||
"description": "Lets an Assist satellite announce a message.",
|
||||
"fields": {
|
||||
"media_id": {
|
||||
"description": "The media ID to announce instead of using text-to-speech.",
|
||||
@@ -94,10 +94,10 @@
|
||||
"name": "Preannounce media ID"
|
||||
}
|
||||
},
|
||||
"name": "Announce"
|
||||
"name": "Announce on satellite"
|
||||
},
|
||||
"ask_question": {
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"description": "Lets an Assist satellite ask a question and get the user's response.",
|
||||
"fields": {
|
||||
"answers": {
|
||||
"description": "Possible answers to the question.",
|
||||
@@ -124,10 +124,10 @@
|
||||
"name": "Question media ID"
|
||||
}
|
||||
},
|
||||
"name": "Ask question"
|
||||
"name": "Ask question on satellite"
|
||||
},
|
||||
"start_conversation": {
|
||||
"description": "Starts a conversation from a satellite.",
|
||||
"description": "Starts a conversation from an Assist satellite.",
|
||||
"fields": {
|
||||
"extra_system_prompt": {
|
||||
"description": "Provide background information to the AI about the request.",
|
||||
@@ -150,13 +150,13 @@
|
||||
"name": "Message"
|
||||
}
|
||||
},
|
||||
"name": "Start conversation"
|
||||
"name": "Start conversation on satellite"
|
||||
}
|
||||
},
|
||||
"title": "Assist satellite",
|
||||
"triggers": {
|
||||
"idle": {
|
||||
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
|
||||
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -165,7 +165,7 @@
|
||||
"name": "Satellite became idle"
|
||||
},
|
||||
"listening": {
|
||||
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
|
||||
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -174,7 +174,7 @@
|
||||
"name": "Satellite started listening"
|
||||
},
|
||||
"processing": {
|
||||
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
|
||||
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
@@ -183,7 +183,7 @@
|
||||
"name": "Satellite started processing"
|
||||
},
|
||||
"responding": {
|
||||
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==67"],
|
||||
"requirements": ["axis==68"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -74,6 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_bucket_name",
|
||||
) from err
|
||||
except exception.BadRequest as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="bad_request",
|
||||
translation_placeholders={"error_message": str(err)},
|
||||
) from err
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -101,8 +101,7 @@ def handle_b2_errors[T](
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except B2Error as err:
|
||||
error_msg = f"Failed during {func.__name__}"
|
||||
raise BackupAgentError(error_msg) from err
|
||||
raise BackupAgentError(f"Failed during {func.__name__}: {err}") from err
|
||||
|
||||
return wrapper
|
||||
|
||||
@@ -170,8 +169,7 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
async def _cleanup_failed_upload(self, filename: str) -> None:
|
||||
"""Clean up a partially uploaded file after upload failure."""
|
||||
_LOGGER.warning(
|
||||
"Attempting to delete partially uploaded main backup file %s "
|
||||
"due to metadata upload failure",
|
||||
"Attempting to delete partially uploaded backup file %s",
|
||||
filename,
|
||||
)
|
||||
try:
|
||||
@@ -180,11 +178,10 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
)
|
||||
await self._hass.async_add_executor_job(uploaded_main_file_info.delete)
|
||||
except B2Error:
|
||||
_LOGGER.debug(
|
||||
"Failed to clean up partially uploaded main backup file %s. "
|
||||
"Manual intervention may be required to delete it from Backblaze B2",
|
||||
_LOGGER.warning(
|
||||
"Failed to clean up partially uploaded backup file %s;"
|
||||
" manual deletion from Backblaze B2 may be required",
|
||||
filename,
|
||||
exc_info=True,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
@@ -256,9 +253,10 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
prefixed_metadata_filename,
|
||||
)
|
||||
|
||||
upload_successful = False
|
||||
tar_uploaded = False
|
||||
try:
|
||||
await self._upload_backup_file(prefixed_tar_filename, open_stream, {})
|
||||
tar_uploaded = True
|
||||
_LOGGER.debug(
|
||||
"Main backup file upload finished for %s", prefixed_tar_filename
|
||||
)
|
||||
@@ -270,15 +268,14 @@ class BackblazeBackupAgent(BackupAgent):
|
||||
_LOGGER.debug(
|
||||
"Metadata file upload finished for %s", prefixed_metadata_filename
|
||||
)
|
||||
upload_successful = True
|
||||
finally:
|
||||
if upload_successful:
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
|
||||
self._invalidate_caches(
|
||||
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
|
||||
)
|
||||
except B2Error:
|
||||
if tar_uploaded:
|
||||
await self._cleanup_failed_upload(prefixed_tar_filename)
|
||||
raise
|
||||
|
||||
def _upload_metadata_file_sync(
|
||||
self, metadata_content: bytes, filename: str
|
||||
|
||||
@@ -174,6 +174,14 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
|
||||
)
|
||||
errors[CONF_BUCKET] = "invalid_bucket_name"
|
||||
except exception.BadRequest as err:
|
||||
_LOGGER.error(
|
||||
"Backblaze B2 API rejected the request for Key ID '%s': %s",
|
||||
user_input[CONF_KEY_ID],
|
||||
err,
|
||||
)
|
||||
errors["base"] = "bad_request"
|
||||
placeholders["error_message"] = str(err)
|
||||
except (
|
||||
exception.B2ConnectionError,
|
||||
exception.B2RequestTimeout,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["b2sdk"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["b2sdk==2.10.1"]
|
||||
"requirements": ["b2sdk==2.10.4"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"bad_request": "The Backblaze B2 API rejected the request: {error_message}",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_bucket_name": "[%key:component::backblaze_b2::exceptions::invalid_bucket_name::message%]",
|
||||
"invalid_capability": "[%key:component::backblaze_b2::exceptions::invalid_capability::message%]",
|
||||
@@ -60,6 +61,9 @@
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"bad_request": {
|
||||
"message": "The Backblaze B2 API rejected the request: {error_message}"
|
||||
},
|
||||
"cannot_connect": {
|
||||
"message": "Cannot connect to endpoint"
|
||||
},
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
|
||||
"requirements": ["cronsim==2.7", "securetar==2026.4.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ from securetar import (
|
||||
SecureTarFile,
|
||||
SecureTarReadError,
|
||||
SecureTarRootKeyContext,
|
||||
get_archive_max_ciphertext_size,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -431,7 +432,9 @@ class _CipherBackupStreamer:
|
||||
|
||||
def size(self) -> int:
|
||||
"""Return the maximum size of the decrypted or encrypted backup."""
|
||||
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
|
||||
return get_archive_max_ciphertext_size( # type: ignore[no-any-return]
|
||||
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
|
||||
)
|
||||
|
||||
def _num_tar_files(self) -> int:
|
||||
"""Return the number of inner tar files."""
|
||||
|
||||
@@ -10,6 +10,7 @@ from bsblan import (
|
||||
BSBLAN,
|
||||
BSBLANAuthError,
|
||||
BSBLANConnectionError,
|
||||
BSBLANError,
|
||||
HotWaterConfig,
|
||||
HotWaterSchedule,
|
||||
HotWaterState,
|
||||
@@ -50,7 +51,7 @@ class BSBLanFastData:
|
||||
|
||||
state: State
|
||||
sensor: Sensor
|
||||
dhw: HotWaterState
|
||||
dhw: HotWaterState | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -111,7 +112,6 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
# This reduces response time significantly (~0.2s per parameter)
|
||||
state = await self.client.state(include=STATE_INCLUDE)
|
||||
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
|
||||
except BSBLANAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
@@ -126,6 +126,19 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
|
||||
translation_placeholders={"host": host},
|
||||
) from err
|
||||
|
||||
# Fetch DHW state separately - device may not support hot water
|
||||
dhw: HotWaterState | None = None
|
||||
try:
|
||||
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
|
||||
except BSBLANError:
|
||||
# Preserve last known DHW state if available (entity may depend on it)
|
||||
if self.data:
|
||||
dhw = self.data.dhw
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) state not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
|
||||
return BSBLanFastData(
|
||||
state=state,
|
||||
sensor=sensor,
|
||||
@@ -159,13 +172,6 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
|
||||
dhw_schedule = await self.client.hot_water_schedule()
|
||||
|
||||
except AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
except (BSBLANConnectionError, BSBLANAuthError) as err:
|
||||
# If config update fails, keep existing data
|
||||
LOGGER.debug(
|
||||
@@ -177,6 +183,13 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
|
||||
return self.data
|
||||
# First fetch failed, return empty data
|
||||
return BSBLanSlowData()
|
||||
except BSBLANError, AttributeError:
|
||||
# Device does not support DHW functionality
|
||||
LOGGER.debug(
|
||||
"DHW (Domestic Hot Water) not available on device at %s",
|
||||
self.config_entry.data[CONF_HOST],
|
||||
)
|
||||
return BSBLanSlowData()
|
||||
|
||||
return BSBLanSlowData(
|
||||
dhw_config=dhw_config,
|
||||
|
||||
@@ -22,7 +22,9 @@ async def async_get_config_entry_diagnostics(
|
||||
"fast_coordinator_data": {
|
||||
"state": data.fast_coordinator.data.state.model_dump(),
|
||||
"sensor": data.fast_coordinator.data.sensor.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump(),
|
||||
"dhw": data.fast_coordinator.data.dhw.model_dump()
|
||||
if data.fast_coordinator.data.dhw
|
||||
else None,
|
||||
},
|
||||
"static": data.static.model_dump() if data.static is not None else None,
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
from homeassistant.helpers.device_registry import (
|
||||
CONNECTION_NETWORK_MAC,
|
||||
DeviceInfo,
|
||||
@@ -10,7 +13,7 @@ from homeassistant.helpers.device_registry import (
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import BSBLanData
|
||||
from .const import DOMAIN
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
from .coordinator import BSBLanCoordinator, BSBLanFastCoordinator, BSBLanSlowCoordinator
|
||||
|
||||
|
||||
@@ -22,7 +25,8 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
def __init__(self, coordinator: _T, data: BSBLanData) -> None:
|
||||
"""Initialize BSBLan entity with device info."""
|
||||
super().__init__(coordinator)
|
||||
host = coordinator.config_entry.data["host"]
|
||||
host = coordinator.config_entry.data[CONF_HOST]
|
||||
port = coordinator.config_entry.data.get(CONF_PORT, DEFAULT_PORT)
|
||||
mac = data.device.MAC
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, mac)},
|
||||
@@ -44,7 +48,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
|
||||
else None
|
||||
),
|
||||
sw_version=data.device.version,
|
||||
configuration_url=f"http://{host}",
|
||||
configuration_url=str(URL.build(scheme="http", host=host, port=port)),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["bsblan"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["python-bsblan==5.1.3"],
|
||||
"requirements": ["python-bsblan==5.1.4"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "bsb-lan*",
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from bsblan import BSBLANError, SetHotWaterParam
|
||||
from bsblan import BSBLANError, HotWaterState, SetHotWaterParam
|
||||
|
||||
from homeassistant.components.water_heater import (
|
||||
STATE_ECO,
|
||||
@@ -46,8 +46,10 @@ async def async_setup_entry(
|
||||
data = entry.runtime_data
|
||||
|
||||
# Only create water heater entity if DHW (Domestic Hot Water) is available
|
||||
# Check if we have any DHW-related data indicating water heater support
|
||||
dhw_data = data.fast_coordinator.data.dhw
|
||||
if dhw_data is None:
|
||||
# Device does not support DHW, skip water heater setup
|
||||
return
|
||||
if (
|
||||
dhw_data.operating_mode is None
|
||||
and dhw_data.nominal_setpoint is None
|
||||
@@ -107,11 +109,21 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
else:
|
||||
self._attr_max_temp = 65.0 # Default maximum
|
||||
|
||||
@property
|
||||
def _dhw(self) -> HotWaterState:
|
||||
"""Return DHW state data.
|
||||
|
||||
This entity is only created when DHW data is available.
|
||||
"""
|
||||
dhw = self.coordinator.data.dhw
|
||||
assert dhw is not None
|
||||
return dhw
|
||||
|
||||
@property
|
||||
def current_operation(self) -> str | None:
|
||||
"""Return current operation."""
|
||||
if (
|
||||
operating_mode := self.coordinator.data.dhw.operating_mode
|
||||
operating_mode := self._dhw.operating_mode
|
||||
) is None or operating_mode.value is None:
|
||||
return None
|
||||
return BSBLAN_TO_HA_OPERATION_MODE.get(operating_mode.value)
|
||||
@@ -119,16 +131,14 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current temperature."""
|
||||
if (
|
||||
current_temp := self.coordinator.data.dhw.dhw_actual_value_top_temperature
|
||||
) is None:
|
||||
if (current_temp := self._dhw.dhw_actual_value_top_temperature) is None:
|
||||
return None
|
||||
return current_temp.value
|
||||
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the temperature we try to reach."""
|
||||
if (target_temp := self.coordinator.data.dhw.nominal_setpoint) is None:
|
||||
if (target_temp := self._dhw.nominal_setpoint) is None:
|
||||
return None
|
||||
return target_temp.value
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
await self.async_set_unique_id(str(user.player_id))
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=user.name, data=user_input)
|
||||
return self.async_create_entry(
|
||||
title=user.name or user.username, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
|
||||
@@ -112,7 +112,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -151,7 +151,7 @@ class ComelitAlarmEntity(
|
||||
if code != str(self.coordinator.api.device_pin):
|
||||
return
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[DISABLE]
|
||||
self._area.index, ALARM_ACTIONS[DISABLE], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
|
||||
@@ -160,7 +160,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[AWAY]
|
||||
self._area.index, ALARM_ACTIONS[AWAY], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
|
||||
@@ -169,7 +169,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[HOME]
|
||||
self._area.index, ALARM_ACTIONS[HOME], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
|
||||
@@ -178,7 +178,7 @@ class ComelitAlarmEntity(
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
await self.coordinator.api.set_zone_status(
|
||||
self._area.index, ALARM_ACTIONS[NIGHT]
|
||||
self._area.index, ALARM_ACTIONS[NIGHT], self._area.anomaly
|
||||
)
|
||||
await self._async_update_state(
|
||||
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiocomelit"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiocomelit==2.0.1"]
|
||||
"requirements": ["aiocomelit==2.0.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydoods"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==12.1.1"]
|
||||
"requirements": ["pydoods==1.0.2", "Pillow==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pyenphase"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pyenphase==2.4.6"],
|
||||
"requirements": ["pyenphase==2.4.8"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_enphase-envoy._tcp.local."
|
||||
|
||||
@@ -259,15 +259,18 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
|
||||
if (color_temp_k := kwargs.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
|
||||
# Do not use kelvin_to_mired here to prevent precision loss
|
||||
color_temp_mired = 1_000_000.0 / color_temp_k
|
||||
data["color_temperature"] = color_temp_mired
|
||||
if color_temp_modes := _filter_color_modes(
|
||||
color_modes, LightColorCapability.COLOR_TEMPERATURE
|
||||
):
|
||||
data["color_temperature"] = color_temp_mired
|
||||
color_modes = color_temp_modes
|
||||
else:
|
||||
# Convert color temperature to explicit cold/warm white
|
||||
# values to avoid ESPHome applying brightness to both
|
||||
# master brightness and white channels (b² effect).
|
||||
# Also send explicit cold/warm white values to avoid
|
||||
# ESPHome applying brightness to both master brightness
|
||||
# and white channels (b² effect). The firmware skips
|
||||
# deriving cwww from color_temperature when the channels
|
||||
# are already set explicitly, but still stores
|
||||
# color_temperature so HA can read it back.
|
||||
data["cold_white"], data["warm_white"] = self._color_temp_to_cold_warm(
|
||||
color_temp_mired
|
||||
)
|
||||
|
||||
@@ -448,10 +448,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
if not attributes.get("MACAddress"):
|
||||
continue
|
||||
|
||||
wan_access_result = None
|
||||
if (wan_access := attributes.get("X_AVM-DE_WANAccess")) is not None:
|
||||
wan_access_result = "granted" in wan_access
|
||||
else:
|
||||
wan_access_result = None
|
||||
# wan_access can be "granted", "denied", "unknown" or "error"
|
||||
if "granted" in wan_access:
|
||||
wan_access_result = True
|
||||
elif "denied" in wan_access:
|
||||
wan_access_result = False
|
||||
|
||||
hosts[attributes["MACAddress"]] = Device(
|
||||
name=attributes["HostName"],
|
||||
|
||||
@@ -10,9 +10,11 @@ from requests.exceptions import RequestException
|
||||
from homeassistant.components.image import ImageEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import DOMAIN, Platform
|
||||
from .coordinator import AvmWrapper, FritzConfigEntry
|
||||
from .entity import FritzBoxBaseEntity
|
||||
|
||||
@@ -22,6 +24,32 @@ _LOGGER = logging.getLogger(__name__)
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
async def _migrate_to_new_unique_id(
|
||||
hass: HomeAssistant, avm_wrapper: AvmWrapper, ssid: str
|
||||
) -> None:
|
||||
"""Migrate old unique id to new unique id."""
|
||||
|
||||
old_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
|
||||
new_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
entity_id = entity_registry.async_get_entity_id(
|
||||
Platform.IMAGE,
|
||||
DOMAIN,
|
||||
old_unique_id,
|
||||
)
|
||||
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
|
||||
_LOGGER.debug(
|
||||
"Migrating guest Wi-Fi image unique_id from [%s] to [%s]",
|
||||
old_unique_id,
|
||||
new_unique_id,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: FritzConfigEntry,
|
||||
@@ -34,6 +62,8 @@ async def async_setup_entry(
|
||||
avm_wrapper.fritz_guest_wifi.get_info
|
||||
)
|
||||
|
||||
await _migrate_to_new_unique_id(hass, avm_wrapper, guest_wifi_info["NewSSID"])
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
FritzGuestWifiQRImage(
|
||||
@@ -60,7 +90,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
|
||||
) -> None:
|
||||
"""Initialize the image entity."""
|
||||
self._attr_name = ssid
|
||||
self._attr_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
|
||||
self._attr_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
|
||||
self._current_qr_bytes: bytes | None = None
|
||||
super().__init__(avm_wrapper, device_friendly_name)
|
||||
ImageEntity.__init__(self, hass)
|
||||
|
||||
@@ -8,6 +8,7 @@ from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
from fritzconnection.lib.fritzstatus import FritzStatus
|
||||
from requests.exceptions import RequestException
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
@@ -145,46 +146,65 @@ def _retrieve_link_attenuation_received_state(
|
||||
|
||||
def _retrieve_cpu_temperature_state(
|
||||
status: FritzStatus, last_value: float | None
|
||||
) -> float:
|
||||
) -> float | None:
|
||||
"""Return the first CPU temperature value."""
|
||||
return status.get_cpu_temperatures()[0] # type: ignore[no-any-return]
|
||||
try:
|
||||
return status.get_cpu_temperatures()[0] # type: ignore[no-any-return]
|
||||
except RequestException:
|
||||
return None
|
||||
|
||||
|
||||
def _is_suitable_cpu_temperature(status: FritzStatus) -> bool:
|
||||
"""Return whether the CPU temperature sensor is suitable."""
|
||||
try:
|
||||
cpu_temp = status.get_cpu_temperatures()[0]
|
||||
except RequestException, IndexError:
|
||||
_LOGGER.debug("CPU temperature not supported by the device")
|
||||
return False
|
||||
if cpu_temp == 0:
|
||||
_LOGGER.debug("CPU temperature returns 0°C, treating as not supported")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzSensorEntityDescription(SensorEntityDescription, FritzEntityDescription):
|
||||
"""Describes Fritz sensor entity."""
|
||||
class FritzConnectionSensorEntityDescription(
|
||||
SensorEntityDescription, FritzEntityDescription
|
||||
):
|
||||
"""Describes Fritz connection sensor entity."""
|
||||
|
||||
is_suitable: Callable[[ConnectionInfo], bool] = lambda info: info.wan_enabled
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
FritzSensorEntityDescription(
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FritzDeviceSensorEntityDescription(
|
||||
SensorEntityDescription, FritzEntityDescription
|
||||
):
|
||||
"""Describes Fritz device sensor entity."""
|
||||
|
||||
is_suitable: Callable[[FritzStatus], bool] = lambda status: True
|
||||
|
||||
|
||||
CONNECTION_SENSOR_TYPES: tuple[FritzConnectionSensorEntityDescription, ...] = (
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="external_ip",
|
||||
translation_key="external_ip",
|
||||
value_fn=_retrieve_external_ip_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="external_ipv6",
|
||||
translation_key="external_ipv6",
|
||||
value_fn=_retrieve_external_ipv6_state,
|
||||
is_suitable=lambda info: info.ipv6_active,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
key="device_uptime",
|
||||
translation_key="device_uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_device_uptime_state,
|
||||
is_suitable=lambda info: True,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="connection_uptime",
|
||||
translation_key="connection_uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_connection_uptime_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="kb_s_sent",
|
||||
translation_key="kb_s_sent",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -192,7 +212,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
value_fn=_retrieve_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="kb_s_received",
|
||||
translation_key="kb_s_received",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -200,21 +220,21 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
value_fn=_retrieve_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="max_kb_s_sent",
|
||||
translation_key="max_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
value_fn=_retrieve_max_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="max_kb_s_received",
|
||||
translation_key="max_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
value_fn=_retrieve_max_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="gb_sent",
|
||||
translation_key="gb_sent",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -222,7 +242,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
value_fn=_retrieve_gb_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="gb_received",
|
||||
translation_key="gb_received",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
@@ -230,7 +250,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
value_fn=_retrieve_gb_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_kb_s_sent",
|
||||
translation_key="link_kb_s_sent",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
@@ -238,7 +258,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_sent_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_kb_s_received",
|
||||
translation_key="link_kb_s_received",
|
||||
native_unit_of_measurement=UnitOfDataRate.KILOBITS_PER_SECOND,
|
||||
@@ -246,7 +266,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_link_kb_s_received_state,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_noise_margin_sent",
|
||||
translation_key="link_noise_margin_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
@@ -255,7 +275,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
value_fn=_retrieve_link_noise_margin_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_noise_margin_received",
|
||||
translation_key="link_noise_margin_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
@@ -264,7 +284,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
value_fn=_retrieve_link_noise_margin_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_attenuation_sent",
|
||||
translation_key="link_attenuation_sent",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
@@ -273,7 +293,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
value_fn=_retrieve_link_attenuation_sent_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
FritzConnectionSensorEntityDescription(
|
||||
key="link_attenuation_received",
|
||||
translation_key="link_attenuation_received",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS,
|
||||
@@ -282,7 +302,17 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
value_fn=_retrieve_link_attenuation_received_state,
|
||||
is_suitable=lambda info: info.wan_enabled and info.connection == DSL_CONNECTION,
|
||||
),
|
||||
FritzSensorEntityDescription(
|
||||
)
|
||||
|
||||
DEVICE_SENSOR_TYPES: tuple[FritzDeviceSensorEntityDescription, ...] = (
|
||||
FritzDeviceSensorEntityDescription(
|
||||
key="device_uptime",
|
||||
translation_key="device_uptime",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=_retrieve_device_uptime_state,
|
||||
),
|
||||
FritzDeviceSensorEntityDescription(
|
||||
key="cpu_temperature",
|
||||
translation_key="cpu_temperature",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
@@ -290,7 +320,7 @@ SENSOR_TYPES: tuple[FritzSensorEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=_retrieve_cpu_temperature_state,
|
||||
is_suitable=lambda info: True,
|
||||
is_suitable=_is_suitable_cpu_temperature,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -305,20 +335,32 @@ async def async_setup_entry(
|
||||
avm_wrapper = entry.runtime_data
|
||||
|
||||
connection_info = await avm_wrapper.async_get_connection_info()
|
||||
|
||||
entities = [
|
||||
FritzBoxSensor(avm_wrapper, entry.title, description)
|
||||
for description in SENSOR_TYPES
|
||||
for description in CONNECTION_SENSOR_TYPES
|
||||
if description.is_suitable(connection_info)
|
||||
]
|
||||
|
||||
fritz_status = avm_wrapper.fritz_status
|
||||
|
||||
def _generate_device_sensors() -> list[FritzBoxSensor]:
|
||||
return [
|
||||
FritzBoxSensor(avm_wrapper, entry.title, description)
|
||||
for description in DEVICE_SENSOR_TYPES
|
||||
if description.is_suitable(fritz_status)
|
||||
]
|
||||
|
||||
entities += await hass.async_add_executor_job(_generate_device_sensors)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class FritzBoxSensor(FritzBoxBaseCoordinatorEntity, SensorEntity):
|
||||
"""Define FRITZ!Box connectivity class."""
|
||||
|
||||
entity_description: FritzSensorEntityDescription
|
||||
entity_description: (
|
||||
FritzConnectionSensorEntityDescription | FritzDeviceSensorEntityDescription
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260325.5"]
|
||||
"requirements": ["home-assistant-frontend==20260325.7"]
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["afsapi==0.2.7"],
|
||||
"requirements": ["afsapi==0.3.1"],
|
||||
"ssdp": [
|
||||
{
|
||||
"st": "urn:schemas-frontier-silicon-com:undok:fsapi:1"
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/generic",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["av==16.0.1", "Pillow==12.1.1"]
|
||||
"requirements": ["av==16.0.1", "Pillow==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
key="mix_export_to_grid",
|
||||
translation_key="mix_export_to_grid",
|
||||
api_key="pacToGridTotal",
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
@@ -80,7 +80,7 @@ SPH_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
|
||||
key="mix_import_from_grid",
|
||||
translation_key="mix_import_from_grid",
|
||||
api_key="pacToUserR",
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.84", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.94", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -68,8 +68,16 @@ PROGRAM_OPTIONS = {
|
||||
),
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: vol.All(int, vol.Range(min=0)),
|
||||
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
|
||||
OptionKey.LAUNDRY_CARE_COMMON_SILENT_MODE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_INTENSIVE_PLUS: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_LESS_IRONING: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_MINI_LOAD: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_PREWASH: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_RINSE_HOLD: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_SOAK: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_WATER_PLUS: bool,
|
||||
}.items()
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,7 @@ set_program_and_options:
|
||||
- cooking_common_program_hood_automatic
|
||||
- cooking_common_program_hood_venting
|
||||
- cooking_common_program_hood_delayed_shut_off
|
||||
- cooking_oven_program_heating_mode_3_d_heating
|
||||
- cooking_oven_program_heating_mode_3_d_hot_air
|
||||
- cooking_oven_program_heating_mode_air_fry
|
||||
- cooking_oven_program_heating_mode_grill_large_area
|
||||
- cooking_oven_program_heating_mode_grill_small_area
|
||||
@@ -210,6 +210,7 @@ set_program_and_options:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_mode:
|
||||
example: heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@@ -222,7 +223,7 @@ set_program_and_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
consumer_products_cleaning_robot_option_reference_map_id:
|
||||
example: consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
example: consumer_products_cleaning_robot_enum_type_available_maps_map_1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@@ -230,9 +231,9 @@ set_program_and_options:
|
||||
translation_key: available_maps
|
||||
options:
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_temp_map
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map2
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map3
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map_1
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map_2
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map_3
|
||||
consumer_products_cleaning_robot_option_cleaning_mode:
|
||||
example: consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
|
||||
required: false
|
||||
@@ -310,7 +311,7 @@ set_program_and_options:
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_94_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_95_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_96_c
|
||||
consumer_products_coffee_maker_option_bean_container:
|
||||
consumer_products_coffee_maker_option_bean_container_selection:
|
||||
example: consumer_products_coffee_maker_enum_type_bean_container_selection_right
|
||||
required: false
|
||||
selector:
|
||||
@@ -468,8 +469,8 @@ set_program_and_options:
|
||||
hood_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_hood_option_venting_level:
|
||||
example: cooking_hood_enum_type_stage_fan_stage01
|
||||
cooking_common_option_hood_venting_level:
|
||||
example: cooking_hood_enum_type_stage_fan_stage_01
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@@ -482,8 +483,8 @@ set_program_and_options:
|
||||
- cooking_hood_enum_type_stage_fan_stage_03
|
||||
- cooking_hood_enum_type_stage_fan_stage_04
|
||||
- cooking_hood_enum_type_stage_fan_stage_05
|
||||
cooking_hood_option_intensive_level:
|
||||
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
cooking_common_option_hood_intensive_level:
|
||||
example: cooking_hood_enum_type_intensive_stage_intensive_stage_1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@@ -491,8 +492,8 @@ set_program_and_options:
|
||||
translation_key: intensive_level
|
||||
options:
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage_off
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage2
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage_1
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage_2
|
||||
oven_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
@@ -567,7 +568,7 @@ set_program_and_options:
|
||||
- laundry_care_washer_enum_type_temperature_ul_hot
|
||||
- laundry_care_washer_enum_type_temperature_ul_extra_hot
|
||||
laundry_care_washer_option_spin_speed:
|
||||
example: laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
example: laundry_care_washer_enum_type_spin_speed_r_p_m_800
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
@@ -611,12 +612,12 @@ set_program_and_options:
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_i_dos1_active:
|
||||
laundry_care_washer_option_i_dos_1_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_i_dos2_active:
|
||||
laundry_care_washer_option_i_dos_2_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
@@ -656,7 +657,7 @@ set_program_and_options:
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_vario_perfect:
|
||||
laundry_care_common_option_vario_perfect:
|
||||
example: laundry_care_common_enum_type_vario_perfect_eco_perfect
|
||||
required: false
|
||||
selector:
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
|
||||
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
|
||||
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
|
||||
"cooking_oven_program_heating_mode_3_d_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_heating%]",
|
||||
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
|
||||
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
|
||||
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
|
||||
"cooking_oven_program_heating_mode_bread_baking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bread_baking%]",
|
||||
@@ -431,7 +431,7 @@
|
||||
}
|
||||
},
|
||||
"bean_container": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_container::name%]",
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_container_selection::name%]",
|
||||
"state": {
|
||||
"consumer_products_coffee_maker_enum_type_bean_container_selection_left": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_left%]",
|
||||
"consumer_products_coffee_maker_enum_type_bean_container_selection_right": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_right%]"
|
||||
@@ -484,9 +484,9 @@
|
||||
"current_map": {
|
||||
"name": "Current map",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_3%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]"
|
||||
}
|
||||
},
|
||||
@@ -557,19 +557,19 @@
|
||||
}
|
||||
},
|
||||
"intensive_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_intensive_level::name%]",
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_common_option_hood_intensive_level::name%]",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage1": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage1%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage2": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage2%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_1": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_1%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_2": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_2%]",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_off": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_off%]"
|
||||
}
|
||||
},
|
||||
"reference_map_id": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_cleaning_robot_option_reference_map_id::name%]",
|
||||
"state": {
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_1%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_2%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_3%]",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]"
|
||||
}
|
||||
},
|
||||
@@ -620,7 +620,7 @@
|
||||
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
|
||||
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
|
||||
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
|
||||
"cooking_oven_program_heating_mode_3_d_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_heating%]",
|
||||
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
|
||||
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
|
||||
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
|
||||
"cooking_oven_program_heating_mode_bread_baking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bread_baking%]",
|
||||
@@ -786,7 +786,7 @@
|
||||
}
|
||||
},
|
||||
"vario_perfect": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_vario_perfect::name%]",
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_common_option_vario_perfect::name%]",
|
||||
"state": {
|
||||
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_eco_perfect%]",
|
||||
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
|
||||
@@ -794,7 +794,7 @@
|
||||
}
|
||||
},
|
||||
"venting_level": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_common_option_hood_venting_level::name%]",
|
||||
"state": {
|
||||
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
|
||||
"cooking_hood_enum_type_stage_fan_stage_01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_01%]",
|
||||
@@ -1272,10 +1272,10 @@
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_hygiene_plus::name%]"
|
||||
},
|
||||
"i_dos1_active": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos1_active::name%]"
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos_1_active::name%]"
|
||||
},
|
||||
"i_dos2_active": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos2_active::name%]"
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos_2_active::name%]"
|
||||
},
|
||||
"intensiv_zone": {
|
||||
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_intensiv_zone::name%]"
|
||||
@@ -1458,9 +1458,9 @@
|
||||
},
|
||||
"available_maps": {
|
||||
"options": {
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "Map 1",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "Map 2",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "Map 3",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "Map 1",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "Map 2",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "Map 3",
|
||||
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "Temporary map"
|
||||
}
|
||||
},
|
||||
@@ -1584,8 +1584,8 @@
|
||||
},
|
||||
"intensive_level": {
|
||||
"options": {
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage1": "Intensive stage 1",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage2": "Intensive stage 2",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_1": "Intensive stage 1",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_2": "Intensive stage 2",
|
||||
"cooking_hood_enum_type_intensive_stage_intensive_stage_off": "Intensive stage off"
|
||||
}
|
||||
},
|
||||
@@ -1629,7 +1629,7 @@
|
||||
"cooking_common_program_hood_automatic": "Automatic",
|
||||
"cooking_common_program_hood_delayed_shut_off": "Delayed shut off",
|
||||
"cooking_common_program_hood_venting": "Venting",
|
||||
"cooking_oven_program_heating_mode_3_d_heating": "3D heating",
|
||||
"cooking_oven_program_heating_mode_3_d_hot_air": "3D hot air",
|
||||
"cooking_oven_program_heating_mode_air_fry": "Air fry",
|
||||
"cooking_oven_program_heating_mode_bottom_heating": "Bottom heating",
|
||||
"cooking_oven_program_heating_mode_bread_baking": "Bread baking",
|
||||
@@ -1892,7 +1892,7 @@
|
||||
"description": "Describes the amount of coffee beans used in a coffee machine program.",
|
||||
"name": "Bean amount"
|
||||
},
|
||||
"consumer_products_coffee_maker_option_bean_container": {
|
||||
"consumer_products_coffee_maker_option_bean_container_selection": {
|
||||
"description": "Defines the preferred bean container.",
|
||||
"name": "Bean container"
|
||||
},
|
||||
@@ -1920,11 +1920,11 @@
|
||||
"description": "Defines if double dispensing is enabled.",
|
||||
"name": "Multiple beverages"
|
||||
},
|
||||
"cooking_hood_option_intensive_level": {
|
||||
"cooking_common_option_hood_intensive_level": {
|
||||
"description": "Defines the intensive setting.",
|
||||
"name": "Intensive level"
|
||||
},
|
||||
"cooking_hood_option_venting_level": {
|
||||
"cooking_common_option_hood_venting_level": {
|
||||
"description": "Defines the required fan setting.",
|
||||
"name": "Venting level"
|
||||
},
|
||||
@@ -1992,15 +1992,19 @@
|
||||
"description": "Defines if the silent mode is activated.",
|
||||
"name": "Silent mode"
|
||||
},
|
||||
"laundry_care_common_option_vario_perfect": {
|
||||
"description": "Defines if a cycle saves energy (Eco Perfect) or time (Speed Perfect).",
|
||||
"name": "Vario perfect"
|
||||
},
|
||||
"laundry_care_dryer_option_drying_target": {
|
||||
"description": "Describes the drying target for a dryer program.",
|
||||
"name": "Drying target"
|
||||
},
|
||||
"laundry_care_washer_option_i_dos1_active": {
|
||||
"laundry_care_washer_option_i_dos_1_active": {
|
||||
"description": "Defines if the detergent feed is activated / deactivated. (i-Dos content 1)",
|
||||
"name": "i-Dos 1 Active"
|
||||
},
|
||||
"laundry_care_washer_option_i_dos2_active": {
|
||||
"laundry_care_washer_option_i_dos_2_active": {
|
||||
"description": "Defines if the detergent feed is activated / deactivated. (i-Dos content 2)",
|
||||
"name": "i-Dos 2 Active"
|
||||
},
|
||||
@@ -2044,10 +2048,6 @@
|
||||
"description": "Defines the temperature of the washing program.",
|
||||
"name": "Temperature"
|
||||
},
|
||||
"laundry_care_washer_option_vario_perfect": {
|
||||
"description": "Defines if a cycle saves energy (Eco Perfect) or time (Speed Perfect).",
|
||||
"name": "Vario perfect"
|
||||
},
|
||||
"laundry_care_washer_option_water_plus": {
|
||||
"description": "Defines if the water plus option is activated.",
|
||||
"name": "Water +"
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohue"],
|
||||
"requirements": ["aiohue==4.8.0"],
|
||||
"requirements": ["aiohue==4.8.1"],
|
||||
"zeroconf": ["_hue._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/image_upload",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["Pillow==12.1.1"]
|
||||
"requirements": ["Pillow==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["imgw_pib==2.0.2"]
|
||||
"requirements": ["imgw_pib==2.1.0"]
|
||||
}
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["incomfortclient"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["incomfort-client==0.6.12"]
|
||||
"requirements": ["incomfort-client==0.7.0"]
|
||||
}
|
||||
|
||||
@@ -92,11 +92,13 @@
|
||||
"central_heating": "Central heating",
|
||||
"central_heating_low": "Central heating low",
|
||||
"central_heating_rf": "Central heating rf",
|
||||
"central_heating_wait": "Central heating waiting",
|
||||
"cv_temperature_too_high_e1": "Temperature too high",
|
||||
"flame_detection_fault_e6": "Flame detection fault",
|
||||
"frost": "Frost protection",
|
||||
"gas_valve_relay_faulty_e29": "Gas valve relay faulty",
|
||||
"gas_valve_relay_faulty_e30": "[%key:component::incomfort::entity::water_heater::boiler::state::gas_valve_relay_faulty_e29%]",
|
||||
"hp_error_recovery": "Heat pump error recovery",
|
||||
"incorrect_fan_speed_e8": "Incorrect fan speed",
|
||||
"no_flame_signal_e4": "No flame signal",
|
||||
"off": "[%key:common::state::off%]",
|
||||
@@ -120,6 +122,7 @@
|
||||
"service": "Service",
|
||||
"shortcut_outside_sensor_temperature_e27": "Shortcut outside temperature sensor",
|
||||
"standby": "[%key:common::state::standby%]",
|
||||
"starting_ch": "Starting central heating",
|
||||
"tapwater": "Tap water",
|
||||
"tapwater_int": "Tap water internal",
|
||||
"unknown": "Unknown"
|
||||
|
||||
@@ -143,7 +143,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry)
|
||||
try:
|
||||
fireplace: UnifiedFireplace = (
|
||||
await UnifiedFireplace.build_fireplace_from_common(
|
||||
_construct_common_data(entry)
|
||||
_construct_common_data(entry),
|
||||
polling_enabled=False,
|
||||
)
|
||||
)
|
||||
LOGGER.debug("Waiting for Fireplace to Initialize")
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import aiohttp
|
||||
from intellifire4py import UnifiedFireplace
|
||||
from intellifire4py.control import IntelliFireController
|
||||
from intellifire4py.model import IntelliFirePollData
|
||||
@@ -11,8 +12,9 @@ from intellifire4py.read import IntelliFireDataProvider
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
|
||||
@@ -52,6 +54,14 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData
|
||||
return self.fireplace.control_api
|
||||
|
||||
async def _async_update_data(self) -> IntelliFirePollData:
|
||||
try:
|
||||
await self.fireplace.perform_poll()
|
||||
except aiohttp.ClientResponseError as err:
|
||||
if err.status == 403:
|
||||
raise ConfigEntryAuthFailed("Authentication failed") from err
|
||||
raise UpdateFailed(f"Error communicating with fireplace: {err}") from err
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
raise UpdateFailed(f"Error communicating with fireplace: {err}") from err
|
||||
return self.fireplace.data
|
||||
|
||||
@property
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==2.0.3"]
|
||||
"requirements": ["pyjvcprojector==2.0.5"]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
"""Representation of an LG soundbar device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_state = MediaPlayerState.OFF
|
||||
_attr_state = MediaPlayerState.ON # Default to ON to ensure compatibility with models that don't send a powerstatus message
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylitterbot"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylitterbot==2025.2.0"]
|
||||
"requirements": ["pylitterbot==2025.3.2"]
|
||||
}
|
||||
|
||||
@@ -4,11 +4,20 @@ from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from pylutron import Button, Keypad, Led, Lutron, OccupancyGroup, Output
|
||||
from pylutron import (
|
||||
Button,
|
||||
Keypad,
|
||||
Led,
|
||||
Lutron,
|
||||
LutronException,
|
||||
OccupancyGroup,
|
||||
Output,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -57,8 +66,12 @@ async def async_setup_entry(
|
||||
pwd = config_entry.data[CONF_PASSWORD]
|
||||
|
||||
lutron_client = Lutron(host, uid, pwd)
|
||||
await hass.async_add_executor_job(lutron_client.load_xml_db)
|
||||
lutron_client.connect()
|
||||
try:
|
||||
await hass.async_add_executor_job(lutron_client.load_xml_db)
|
||||
lutron_client.connect()
|
||||
except LutronException as ex:
|
||||
raise ConfigEntryNotReady(f"Failed to connect to Lutron repeater: {ex}") from ex
|
||||
|
||||
_LOGGER.debug("Connected to main repeater at %s", host)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pylutron"],
|
||||
"requirements": ["pylutron==0.4.0"],
|
||||
"requirements": ["pylutron==0.4.1"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["matrix_client"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==12.1.1", "aiofiles==24.1.0"]
|
||||
"requirements": ["matrix-nio==0.25.2", "Pillow==12.2.0", "aiofiles==24.1.0"]
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8027),
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x138C, 0x0101),
|
||||
}
|
||||
|
||||
SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
@@ -156,6 +157,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
|
||||
(0x1209, 0x8028),
|
||||
(0x1209, 0x8029),
|
||||
(0x131A, 0x1000),
|
||||
(0x138C, 0x0101),
|
||||
}
|
||||
|
||||
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum
|
||||
|
||||
@@ -323,7 +323,11 @@ DISCOVERY_SCHEMAS = [
|
||||
required_attributes=(
|
||||
clusters.FanControl.Attributes.FanMode,
|
||||
clusters.FanControl.Attributes.PercentCurrent,
|
||||
clusters.FanControl.Attributes.PercentSetting,
|
||||
),
|
||||
# PercentSetting SHALL be null when FanMode is Auto (spec 4.4.6.3),
|
||||
# so allow null values to not block discovery in that state.
|
||||
allow_none_value=True,
|
||||
optional_attributes=(
|
||||
clusters.FanControl.Attributes.SpeedSetting,
|
||||
clusters.FanControl.Attributes.RockSetting,
|
||||
|
||||
@@ -168,10 +168,15 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
|
||||
self._attr_target_temperature = self._get_temperature_in_degrees(
|
||||
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
|
||||
)
|
||||
system_mode = self.get_matter_attribute_value(
|
||||
clusters.Thermostat.Attributes.SystemMode
|
||||
)
|
||||
boost_state = self.get_matter_attribute_value(
|
||||
clusters.WaterHeaterManagement.Attributes.BoostState
|
||||
)
|
||||
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
|
||||
if system_mode == clusters.Thermostat.Enums.SystemModeEnum.kOff:
|
||||
self._attr_current_operation = STATE_OFF
|
||||
elif boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
|
||||
self._attr_current_operation = STATE_HIGH_DEMAND
|
||||
else:
|
||||
self._attr_current_operation = STATE_ECO
|
||||
@@ -218,6 +223,7 @@ DISCOVERY_SCHEMAS = [
|
||||
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
|
||||
clusters.Thermostat.Attributes.LocalTemperature,
|
||||
clusters.Thermostat.Attributes.SystemMode,
|
||||
clusters.WaterHeaterManagement.Attributes.FeatureMap,
|
||||
),
|
||||
optional_attributes=(
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
},
|
||||
"clear_playlist": {
|
||||
"description": "Removes all items from a media player's playlist.",
|
||||
"name": "Clear playlist"
|
||||
"name": "Clear media player playlist"
|
||||
},
|
||||
"join": {
|
||||
"description": "Groups media players together for synchronous playback. Only works on supported multiroom audio systems.",
|
||||
@@ -270,44 +270,44 @@
|
||||
"name": "Group members"
|
||||
}
|
||||
},
|
||||
"name": "Join"
|
||||
"name": "Join media players"
|
||||
},
|
||||
"media_next_track": {
|
||||
"description": "Selects the next track.",
|
||||
"name": "Next"
|
||||
"description": "Selects the next track on a media player.",
|
||||
"name": "Next track"
|
||||
},
|
||||
"media_pause": {
|
||||
"description": "Pauses playback on a media player.",
|
||||
"name": "[%key:common::action::pause%]"
|
||||
"name": "Pause media"
|
||||
},
|
||||
"media_play": {
|
||||
"description": "Starts playback on a media player.",
|
||||
"name": "Play"
|
||||
"name": "Play media"
|
||||
},
|
||||
"media_play_pause": {
|
||||
"description": "Toggles play/pause on a media player.",
|
||||
"name": "Play/Pause"
|
||||
"name": "Play/Pause media"
|
||||
},
|
||||
"media_previous_track": {
|
||||
"description": "Selects the previous track.",
|
||||
"name": "Previous"
|
||||
"description": "Selects the previous track on a media player.",
|
||||
"name": "Previous track"
|
||||
},
|
||||
"media_seek": {
|
||||
"description": "Allows you to go to a different part of the media that is currently playing.",
|
||||
"description": "Allows you to go to a different part of the media that is currently playing on a media player.",
|
||||
"fields": {
|
||||
"seek_position": {
|
||||
"description": "Target position in the currently playing media. The format is platform dependent.",
|
||||
"name": "Position"
|
||||
}
|
||||
},
|
||||
"name": "Seek"
|
||||
"name": "Seek media"
|
||||
},
|
||||
"media_stop": {
|
||||
"description": "Stops playback on a media player.",
|
||||
"name": "[%key:common::action::stop%]"
|
||||
"name": "Stop media"
|
||||
},
|
||||
"play_media": {
|
||||
"description": "Starts playing specified media.",
|
||||
"description": "Starts playing specified media on a media player.",
|
||||
"fields": {
|
||||
"announce": {
|
||||
"description": "If the media should be played as an announcement.",
|
||||
@@ -325,14 +325,14 @@
|
||||
"name": "Play media"
|
||||
},
|
||||
"repeat_set": {
|
||||
"description": "Sets the repeat mode.",
|
||||
"description": "Sets the repeat mode of a media player.",
|
||||
"fields": {
|
||||
"repeat": {
|
||||
"description": "Whether the media (one or all) should be played in a loop or not.",
|
||||
"name": "Repeat mode"
|
||||
}
|
||||
},
|
||||
"name": "Set repeat"
|
||||
"name": "Set media player repeat"
|
||||
},
|
||||
"search_media": {
|
||||
"description": "Searches the available media.",
|
||||
@@ -357,14 +357,14 @@
|
||||
"name": "Search media"
|
||||
},
|
||||
"select_sound_mode": {
|
||||
"description": "Selects a specific sound mode.",
|
||||
"description": "Selects a specific sound mode of a media player.",
|
||||
"fields": {
|
||||
"sound_mode": {
|
||||
"description": "Name of the sound mode to switch to.",
|
||||
"name": "Sound mode"
|
||||
}
|
||||
},
|
||||
"name": "Select sound mode"
|
||||
"name": "Select media player sound mode"
|
||||
},
|
||||
"select_source": {
|
||||
"description": "Sends a media player the command to change the input source.",
|
||||
@@ -374,37 +374,37 @@
|
||||
"name": "Source"
|
||||
}
|
||||
},
|
||||
"name": "Select source"
|
||||
"name": "Select media player source"
|
||||
},
|
||||
"shuffle_set": {
|
||||
"description": "Enables or disables the shuffle mode.",
|
||||
"description": "Enables or disables the shuffle mode of a media player.",
|
||||
"fields": {
|
||||
"shuffle": {
|
||||
"description": "Whether the media should be played in randomized order or not.",
|
||||
"name": "Shuffle mode"
|
||||
}
|
||||
},
|
||||
"name": "Set shuffle"
|
||||
"name": "Set media player shuffle"
|
||||
},
|
||||
"toggle": {
|
||||
"description": "Toggles a media player on/off.",
|
||||
"name": "[%key:common::action::toggle%]"
|
||||
"name": "Toggle media player"
|
||||
},
|
||||
"turn_off": {
|
||||
"description": "Turns off the power of a media player.",
|
||||
"name": "[%key:common::action::turn_off%]"
|
||||
"name": "Turn off media player"
|
||||
},
|
||||
"turn_on": {
|
||||
"description": "Turns on the power of a media player.",
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
"name": "Turn on media player"
|
||||
},
|
||||
"unjoin": {
|
||||
"description": "Removes a media player from a group. Only works on platforms which support player groups.",
|
||||
"name": "Unjoin"
|
||||
"name": "Unjoin media player"
|
||||
},
|
||||
"volume_down": {
|
||||
"description": "Turns down the volume of a media player.",
|
||||
"name": "Turn down volume"
|
||||
"name": "Turn down media player volume"
|
||||
},
|
||||
"volume_mute": {
|
||||
"description": "Mutes or unmutes a media player.",
|
||||
@@ -414,7 +414,7 @@
|
||||
"name": "Muted"
|
||||
}
|
||||
},
|
||||
"name": "Mute/unmute volume"
|
||||
"name": "Mute/unmute media player"
|
||||
},
|
||||
"volume_set": {
|
||||
"description": "Sets the volume level of a media player.",
|
||||
@@ -424,11 +424,11 @@
|
||||
"name": "Level"
|
||||
}
|
||||
},
|
||||
"name": "Set volume"
|
||||
"name": "Set media player volume"
|
||||
},
|
||||
"volume_up": {
|
||||
"description": "Turns up the volume of a media player.",
|
||||
"name": "Turn up volume"
|
||||
"name": "Turn up media player volume"
|
||||
}
|
||||
},
|
||||
"title": "Media player",
|
||||
|
||||
@@ -19,9 +19,13 @@ LIGHT = "light"
|
||||
LIGHT_ON = 1
|
||||
LIGHT_OFF = 2
|
||||
|
||||
# API "no reading" sentinels. Most temperatures use centidegrees (-32768 -> -327.68 °C).
|
||||
# Some devices report the int16 minimum already in degrees after scaling (-3276800 raw -> -32768 °C).
|
||||
DISABLED_TEMP_ENTITIES = (
|
||||
-32768 / 100,
|
||||
-32766 / 100,
|
||||
-32768.0,
|
||||
-32766.0,
|
||||
)
|
||||
|
||||
|
||||
@@ -368,9 +372,11 @@ class ProgramPhaseSteamOvenCombi(MieleEnum, missing_to_none=True):
|
||||
energy_save = 3084
|
||||
pre_heating = 3099
|
||||
|
||||
steam_reduction = 3863
|
||||
steam_reduction = 3863, 7959
|
||||
waiting_for_start = 7939
|
||||
heating_up_phase = 7940
|
||||
drying = 7961
|
||||
rinse = 7962
|
||||
|
||||
|
||||
class ProgramPhaseSteamOvenMicro(MieleEnum, missing_to_none=True):
|
||||
@@ -494,7 +500,7 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
|
||||
intensive = 1, 26, 205
|
||||
maintenance = 2, 27, 214
|
||||
eco = 3, 22, 28, 200
|
||||
automatic = 6, 7, 31, 32, 202
|
||||
automatic = 6, 7, 31, 32, 201, 202
|
||||
solar_save = 9, 34
|
||||
gentle = 10, 35, 210
|
||||
extra_quiet = 11, 36, 207
|
||||
@@ -625,7 +631,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
rinse = 333
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
hydroclean = 341
|
||||
hydroclean = 341, 2434
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359, 2023
|
||||
|
||||
@@ -93,7 +93,14 @@ def _convert_temperature(
|
||||
"""Convert temperature object to readable value."""
|
||||
if index >= len(value_list):
|
||||
return None
|
||||
raw_value = cast(int, value_list[index].temperature) / 100.0
|
||||
raw = value_list[index].temperature
|
||||
if raw is None:
|
||||
return None
|
||||
try:
|
||||
raw_centi = int(raw)
|
||||
except TypeError, ValueError:
|
||||
return None
|
||||
raw_value = raw_centi / 100.0
|
||||
if raw_value in DISABLED_TEMP_ENTITIES:
|
||||
return None
|
||||
return raw_value
|
||||
@@ -639,6 +646,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleDevice], ...]] = (
|
||||
MieleAppliance.OVEN,
|
||||
MieleAppliance.OVEN_MICROWAVE,
|
||||
MieleAppliance.STEAM_OVEN_COMBI,
|
||||
MieleAppliance.STEAM_OVEN_MK2,
|
||||
),
|
||||
description=MieleSensorDescription(
|
||||
key="state_core_temperature",
|
||||
@@ -840,9 +848,9 @@ async def async_setup_entry(
|
||||
and definition.description.value_fn(device) is None
|
||||
and definition.description.zone != 1
|
||||
):
|
||||
# all appliances supporting temperature have at least zone 1, for other zones
|
||||
# don't create entity if API signals that datapoint is disabled, unless the sensor
|
||||
# already appeared in the past (= it provided a valid value)
|
||||
# Optional temperature datapoints (extra fridge zones, oven food probe): only
|
||||
# create the entity after the API first reports a valid reading, then keep it
|
||||
# so state can return to unknown when the datapoint is inactive.
|
||||
return _is_entity_registered(unique_id)
|
||||
if (
|
||||
definition.description.key == "state_plate_step"
|
||||
|
||||
@@ -21,7 +21,11 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
|
||||
from homeassistant.core import HassJobType, HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, discovery_flow
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery_flow,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import (
|
||||
async_dispatcher_connect,
|
||||
async_dispatcher_send,
|
||||
@@ -47,7 +51,10 @@ from .const import (
|
||||
)
|
||||
from .models import DATA_MQTT, MqttComponentConfig, MqttOriginInfo, ReceiveMessage
|
||||
from .schemas import DEVICE_DISCOVERY_SCHEMA, MQTT_ORIGIN_INFO_SCHEMA, SHARED_OPTIONS
|
||||
from .util import async_forward_entry_setup_and_setup_discovery
|
||||
from .util import (
|
||||
async_cleanup_device_registry,
|
||||
async_forward_entry_setup_and_setup_discovery,
|
||||
)
|
||||
|
||||
ABBREVIATIONS_SET = set(ABBREVIATIONS)
|
||||
DEVICE_ABBREVIATIONS_SET = set(DEVICE_ABBREVIATIONS)
|
||||
@@ -565,7 +572,28 @@ async def async_start( # noqa: C901
|
||||
elif payload:
|
||||
_async_add_component(payload)
|
||||
else:
|
||||
# Unhandled discovery message
|
||||
entity_registry = er.async_get(hass)
|
||||
if (
|
||||
(
|
||||
entity_hash := mqtt_data.discovery_discovered_and_disabled.pop(
|
||||
discovery_hash, None
|
||||
)
|
||||
)
|
||||
and (entity_id := entity_registry.entities.get_entity_id(entity_hash))
|
||||
and (entity_entry := entity_registry.async_get(entity_id))
|
||||
):
|
||||
# Cleanup discovered disabled entity / device
|
||||
entity_registry.async_remove(entity_id)
|
||||
hass.async_create_task(
|
||||
async_cleanup_device_registry(
|
||||
hass,
|
||||
device_id=entity_entry.device_id,
|
||||
config_entry_id=entity_entry.config_entry_id,
|
||||
),
|
||||
name=f"Check for cleanup device registry for {entity_id}",
|
||||
)
|
||||
|
||||
# Finish handling discovery message
|
||||
async_dispatcher_send(
|
||||
hass, MQTT_DISCOVERY_DONE.format(*discovery_hash), None
|
||||
)
|
||||
|
||||
@@ -125,7 +125,11 @@ from .subscription import (
|
||||
async_subscribe_topics_internal,
|
||||
async_unsubscribe_topics,
|
||||
)
|
||||
from .util import learn_more_url, mqtt_config_entry_enabled
|
||||
from .util import (
|
||||
async_cleanup_device_registry,
|
||||
learn_more_url,
|
||||
mqtt_config_entry_enabled,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -712,34 +716,6 @@ class MqttAvailabilityMixin(Entity):
|
||||
return self._available_latest
|
||||
|
||||
|
||||
async def cleanup_device_registry(
|
||||
hass: HomeAssistant, device_id: str | None, config_entry_id: str | None
|
||||
) -> None:
|
||||
"""Clean up the device registry after MQTT removal.
|
||||
|
||||
Remove MQTT from the device registry entry if there are no remaining
|
||||
entities, triggers or tags.
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
from . import device_trigger, tag # noqa: PLC0415
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
if (
|
||||
device_id
|
||||
and device_id not in device_registry.deleted_devices
|
||||
and config_entry_id
|
||||
and not er.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=False
|
||||
)
|
||||
and not await device_trigger.async_get_triggers(hass, device_id)
|
||||
and not tag.async_has_tags(hass, device_id)
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device_id, remove_config_entry_id=config_entry_id
|
||||
)
|
||||
|
||||
|
||||
def get_discovery_hash(discovery_data: DiscoveryInfoType) -> tuple[str, str]:
|
||||
"""Get the discovery hash from the discovery data."""
|
||||
discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
|
||||
@@ -994,7 +970,7 @@ class MqttDiscoveryDeviceUpdateMixin(ABC):
|
||||
if not self._skip_device_removal:
|
||||
# Prevent a second cleanup round after the device is removed
|
||||
self._skip_device_removal = True
|
||||
await cleanup_device_registry(
|
||||
await async_cleanup_device_registry(
|
||||
self.hass, self._device_id, self._config_entry_id
|
||||
)
|
||||
|
||||
@@ -1062,7 +1038,7 @@ class MqttDiscoveryUpdateMixin(Entity):
|
||||
entity_registry = er.async_get(self.hass)
|
||||
if entity_entry := entity_registry.async_get(self.entity_id):
|
||||
entity_registry.async_remove(self.entity_id)
|
||||
await cleanup_device_registry(
|
||||
await async_cleanup_device_registry(
|
||||
self.hass, entity_entry.device_id, entity_entry.config_entry_id
|
||||
)
|
||||
else:
|
||||
@@ -1410,7 +1386,7 @@ class MqttEntity(
|
||||
self._setup_common_attributes_from_config(self._config)
|
||||
|
||||
# Initialize entity_id from config
|
||||
self._init_entity_id()
|
||||
self._init_entity_registry(discovery_data)
|
||||
|
||||
# Initialize mixin classes
|
||||
MqttAttributesMixin.__init__(self, config)
|
||||
@@ -1421,16 +1397,19 @@ class MqttEntity(
|
||||
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
|
||||
ensure_via_device_exists(self.hass, self.device_info, self._config_entry)
|
||||
|
||||
def _init_entity_id(self) -> None:
|
||||
"""Set entity_id from default_entity_id if defined in config."""
|
||||
def _init_entity_registry(self, discovery_data: DiscoveryInfoType | None) -> None:
|
||||
"""Set entity_id from default_entity_id if defined in config.
|
||||
|
||||
Check if the previous registry state was disabled
|
||||
or is set to be disabled initially for discovered entities.
|
||||
"""
|
||||
object_id: str
|
||||
default_entity_id: str | None
|
||||
if (default_entity_id := self._config.get(CONF_DEFAULT_ENTITY_ID)) is None:
|
||||
return
|
||||
_, _, object_id = default_entity_id.partition(".")
|
||||
self.entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, object_id, None, self.hass
|
||||
)
|
||||
if default_entity_id := self._config.get(CONF_DEFAULT_ENTITY_ID):
|
||||
_, _, object_id = default_entity_id.partition(".")
|
||||
self.entity_id = async_generate_entity_id(
|
||||
self._entity_id_format, object_id, None, self.hass
|
||||
)
|
||||
|
||||
if self.unique_id is None:
|
||||
return
|
||||
@@ -1446,6 +1425,42 @@ class MqttEntity(
|
||||
# if a deleted entity was found
|
||||
self._update_registry_entity_id = self.entity_id
|
||||
|
||||
if (
|
||||
self._config[CONF_ENABLED_BY_DEFAULT]
|
||||
and deleted_entry
|
||||
and deleted_entry.disabled_by is not None
|
||||
):
|
||||
# Enable previous deleted entity and enable it
|
||||
recreated_entry = entity_registry.async_get_or_create(
|
||||
entity_platform, DOMAIN, self.unique_id
|
||||
)
|
||||
entity_registry.async_update_entity(
|
||||
recreated_entry.entity_id,
|
||||
disabled_by=None,
|
||||
)
|
||||
|
||||
if discovery_data is None:
|
||||
return
|
||||
|
||||
# Allow a disabled entity and device registry
|
||||
# to be cleaned up via MQTT discovery
|
||||
if existing_entity_id := entity_registry.async_get_entity_id(
|
||||
entity_platform, DOMAIN, self.unique_id
|
||||
):
|
||||
existing_entry = entity_registry.async_get(existing_entity_id)
|
||||
|
||||
# Store discovery hash for new entities that are initial disabled
|
||||
# or for entries that are disabled in the registry,
|
||||
# so they can be removed with an empty discovery payload
|
||||
if (
|
||||
existing_entity_id is None
|
||||
or (existing_entry and existing_entry.disabled_by is not None)
|
||||
) and not self._config[CONF_ENABLED_BY_DEFAULT]:
|
||||
mqtt_data = self.hass.data[DATA_MQTT]
|
||||
mqtt_data.discovery_discovered_and_disabled[
|
||||
discovery_data[ATTR_DISCOVERY_HASH]
|
||||
] = (entity_platform, DOMAIN, self.unique_id)
|
||||
|
||||
@final
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to MQTT events."""
|
||||
@@ -1576,7 +1591,7 @@ class MqttEntity(
|
||||
"""(Re)Setup the common attributes for the entity."""
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_entity_registry_enabled_default = bool(
|
||||
config.get(CONF_ENABLED_BY_DEFAULT)
|
||||
config.get(CONF_ENABLED_BY_DEFAULT, True)
|
||||
)
|
||||
self._attr_icon = config.get(CONF_ICON)
|
||||
self._attr_entity_picture = config.get(CONF_ENTITY_PICTURE)
|
||||
|
||||
@@ -146,7 +146,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
|
||||
|
||||
_fixed_color_mode: ColorMode | str | None = None
|
||||
_flash_times: dict[str, int | None]
|
||||
_topic: dict[str, str | None]
|
||||
_optimistic: bool
|
||||
@@ -190,6 +189,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
|
||||
self._attr_supported_features |= (
|
||||
config[CONF_TRANSITION] and LightEntityFeature.TRANSITION
|
||||
)
|
||||
self._attr_color_mode = ColorMode.UNKNOWN
|
||||
if supported_color_modes := self._config.get(CONF_SUPPORTED_COLOR_MODES):
|
||||
self._attr_supported_color_modes = supported_color_modes
|
||||
if self.supported_color_modes and len(self.supported_color_modes) == 1:
|
||||
|
||||
@@ -400,6 +400,12 @@ class MqttData:
|
||||
)
|
||||
device_triggers: dict[str, Trigger] = field(default_factory=dict)
|
||||
data_config_flow_lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
||||
# Attribute `discovery_discovered_and_disabled` maps a discovery hash to
|
||||
# the entity registry index, which is a tuple (entity_platform, "mqtt", unique_id)
|
||||
# It allows to cleanup disabled entities when an empty payload is received.
|
||||
discovery_discovered_and_disabled: dict[tuple[str, str], tuple[str, str, str]] = (
|
||||
field(default_factory=dict)
|
||||
)
|
||||
discovery_already_discovered: set[tuple[str, str]] = field(default_factory=set)
|
||||
discovery_pending_discovered: dict[tuple[str, str], PendingDiscovered] = field(
|
||||
default_factory=dict
|
||||
|
||||
@@ -17,7 +17,12 @@ from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import MAX_LENGTH_STATE_STATE, STATE_UNKNOWN, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, template
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
template,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
|
||||
@@ -421,3 +426,31 @@ def migrate_certificate_file_to_content(file_name_or_auto: str) -> str | None:
|
||||
def learn_more_url(platform: str) -> str:
|
||||
"""Return the URL for the platform specific MQTT documentation."""
|
||||
return f"https://www.home-assistant.io/integrations/{platform}.mqtt/"
|
||||
|
||||
|
||||
async def async_cleanup_device_registry(
|
||||
hass: HomeAssistant, device_id: str | None, config_entry_id: str | None
|
||||
) -> None:
|
||||
"""Clean up the device registry after MQTT removal.
|
||||
|
||||
Remove MQTT from the device registry entry if there are no remaining
|
||||
entities, triggers or tags.
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
from . import device_trigger, tag # noqa: PLC0415
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
if (
|
||||
device_id
|
||||
and device_id not in device_registry.deleted_devices
|
||||
and config_entry_id
|
||||
and not er.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=False
|
||||
)
|
||||
and not await device_trigger.async_get_triggers(hass, device_id)
|
||||
and not tag.async_has_tags(hass, device_id)
|
||||
):
|
||||
device_registry.async_update_device(
|
||||
device_id, remove_config_entry_id=config_entry_id
|
||||
)
|
||||
|
||||
@@ -104,12 +104,8 @@ async def async_setup_entry(
|
||||
def _create_entity(device: dict) -> MyNeoSelect:
|
||||
"""Create a select entity for a device."""
|
||||
if device["model"] == "EWS":
|
||||
# According to the MyNeomitis API, EWS "relais" devices expose a "relayMode"
|
||||
# field in their state, while "pilote" devices do not. We therefore use the
|
||||
# presence of "relayMode" as an explicit heuristic to distinguish relais
|
||||
# from pilote devices. If the upstream API changes this behavior, this
|
||||
# detection logic must be revisited.
|
||||
if "relayMode" in device.get("state", {}):
|
||||
state = device.get("state") or {}
|
||||
if state.get("deviceType") == 0:
|
||||
description = SELECT_TYPES["relais"]
|
||||
else:
|
||||
description = SELECT_TYPES["pilote"]
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pynintendoauth", "pynintendoparental"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.3"]
|
||||
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.4"]
|
||||
}
|
||||
|
||||
@@ -30,12 +30,12 @@ def _validate_input(data: dict[str, Any]) -> None:
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
nzbget_api = NZBGetAPI(
|
||||
data[CONF_HOST],
|
||||
data.get(CONF_USERNAME),
|
||||
data.get(CONF_PASSWORD),
|
||||
data[CONF_SSL],
|
||||
data[CONF_VERIFY_SSL],
|
||||
data[CONF_PORT],
|
||||
host=data[CONF_HOST],
|
||||
username=data.get(CONF_USERNAME),
|
||||
password=data.get(CONF_PASSWORD),
|
||||
secure=data[CONF_SSL],
|
||||
verify_certificate=data[CONF_VERIFY_SSL],
|
||||
port=data[CONF_PORT],
|
||||
)
|
||||
|
||||
nzbget_api.version()
|
||||
|
||||
@@ -35,12 +35,12 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
) -> None:
|
||||
"""Initialize global NZBGet data updater."""
|
||||
self.nzbget = NZBGetAPI(
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.data.get(CONF_USERNAME),
|
||||
config_entry.data.get(CONF_PASSWORD),
|
||||
config_entry.data[CONF_SSL],
|
||||
config_entry.data[CONF_VERIFY_SSL],
|
||||
config_entry.data[CONF_PORT],
|
||||
host=config_entry.data[CONF_HOST],
|
||||
username=config_entry.data.get(CONF_USERNAME),
|
||||
password=config_entry.data.get(CONF_PASSWORD),
|
||||
secure=config_entry.data[CONF_SSL],
|
||||
verify_certificate=config_entry.data[CONF_VERIFY_SSL],
|
||||
port=config_entry.data[CONF_PORT],
|
||||
)
|
||||
|
||||
self._completed_downloads_init = False
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["oasatelematics"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["oasatelematics==0.3"]
|
||||
"requirements": ["oasatelematics==0.4"]
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
|
||||
OpenEVSESensorDescription(
|
||||
key="charging_current",
|
||||
translation_key="charging_current",
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.MILLIAMPERE,
|
||||
suggested_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda ev: ev.charging_current,
|
||||
@@ -117,7 +118,8 @@ SENSOR_TYPES: tuple[OpenEVSESensorDescription, ...] = (
|
||||
OpenEVSESensorDescription(
|
||||
key="charging_power",
|
||||
translation_key="charging_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
native_unit_of_measurement=UnitOfPower.MILLIWATT,
|
||||
suggested_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda ev: ev.charging_power,
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["opower==0.18.0"]
|
||||
"requirements": ["opower==0.18.1"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_picnic_api2"],
|
||||
"requirements": ["python-picnic-api2==1.3.1"]
|
||||
"requirements": ["python-picnic-api2==1.3.4"]
|
||||
}
|
||||
|
||||
@@ -168,15 +168,34 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
docker_version,
|
||||
docker_info,
|
||||
docker_system_df,
|
||||
stacks,
|
||||
) = await asyncio.gather(
|
||||
self.portainer.get_containers(endpoint.id),
|
||||
self.portainer.docker_version(endpoint.id),
|
||||
self.portainer.docker_info(endpoint.id),
|
||||
self.portainer.docker_system_df(endpoint.id),
|
||||
self.portainer.get_stacks(endpoint.id),
|
||||
)
|
||||
|
||||
stack_requests = [self.portainer.get_stacks(endpoint_id=endpoint.id)]
|
||||
swarm_id = (
|
||||
docker_info.swarm.cluster.get("ID")
|
||||
if docker_info.swarm
|
||||
and docker_info.swarm.control_available
|
||||
and docker_info.swarm.cluster
|
||||
else None
|
||||
)
|
||||
if swarm_id:
|
||||
stack_requests.append(
|
||||
self.portainer.get_stacks(
|
||||
endpoint_id=endpoint.id, swarm_id=swarm_id
|
||||
)
|
||||
)
|
||||
|
||||
stacks = [
|
||||
stack
|
||||
for result in await asyncio.gather(*stack_requests)
|
||||
for stack in result
|
||||
]
|
||||
|
||||
prev_endpoint = self.data.get(endpoint.id) if self.data else None
|
||||
container_map: dict[str, PortainerContainerData] = {}
|
||||
stack_map: dict[str, PortainerStackData] = {
|
||||
|
||||
@@ -189,6 +189,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ProxmoxConfigEntry) ->
|
||||
# Migration for additional configuration options added to support API tokens
|
||||
if entry.version < 3:
|
||||
data = dict(entry.data)
|
||||
# If CONF_REALM wasn't there yet, extract from username
|
||||
if CONF_REALM not in data:
|
||||
data[CONF_REALM] = DEFAULT_REALM
|
||||
if "@" in data.get(CONF_USERNAME, ""):
|
||||
username, realm = data[CONF_USERNAME].split("@", 1)
|
||||
data[CONF_USERNAME] = username
|
||||
data[CONF_REALM] = realm.lower()
|
||||
|
||||
realm = data[CONF_REALM].lower()
|
||||
|
||||
# If the realm is one of the base providers, set the provider to match the realm.
|
||||
|
||||
@@ -28,14 +28,17 @@ from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
|
||||
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
|
||||
from .helpers import is_granted
|
||||
|
||||
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
|
||||
"""Class to hold Proxmox node button description."""
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission: ProxmoxPermission = ProxmoxPermission.SYSPOWER
|
||||
permission_raise: str = "no_permission_node_power"
|
||||
permission_target: str = "nodes"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -44,7 +47,8 @@ class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
permission_raise: str = NO_PERM_VM_LXC_POWER
|
||||
permission_target: str = "vms"
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -53,7 +57,8 @@ class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
|
||||
|
||||
press_action: Callable[[ProxmoxCoordinator, str, int], None]
|
||||
permission: ProxmoxPermission = ProxmoxPermission.POWER
|
||||
permission_raise: str = "no_permission_vm_lxc_power"
|
||||
permission_raise: str = NO_PERM_VM_LXC_POWER
|
||||
permission_target: str = "vms"
|
||||
|
||||
|
||||
NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
@@ -76,6 +81,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
ProxmoxNodeButtonNodeEntityDescription(
|
||||
key="start_all",
|
||||
translation_key="start_all",
|
||||
permission=ProxmoxPermission.POWER,
|
||||
permission_raise=NO_PERM_VM_LXC_POWER,
|
||||
permission_target="vms",
|
||||
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
||||
node
|
||||
).startall.post(),
|
||||
@@ -84,6 +92,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
ProxmoxNodeButtonNodeEntityDescription(
|
||||
key="stop_all",
|
||||
translation_key="stop_all",
|
||||
permission=ProxmoxPermission.POWER,
|
||||
permission_raise=NO_PERM_VM_LXC_POWER,
|
||||
permission_target="vms",
|
||||
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
||||
node
|
||||
).stopall.post(),
|
||||
@@ -92,6 +103,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
|
||||
ProxmoxNodeButtonNodeEntityDescription(
|
||||
key="suspend_all",
|
||||
translation_key="suspend_all",
|
||||
permission=ProxmoxPermission.POWER,
|
||||
permission_raise=NO_PERM_VM_LXC_POWER,
|
||||
permission_target="vms",
|
||||
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
|
||||
node
|
||||
).suspendall.post(),
|
||||
@@ -327,7 +341,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
|
||||
node_id = self._node_data.node["node"]
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="nodes",
|
||||
p_type=self.entity_description.permission_target,
|
||||
p_id=node_id,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
@@ -352,7 +366,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
|
||||
vmid = self.vm_data["vmid"]
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_type=self.entity_description.permission_target,
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
@@ -379,7 +393,7 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
|
||||
# Container power actions fall under vms
|
||||
if not is_granted(
|
||||
self.coordinator.permissions,
|
||||
p_type="vms",
|
||||
p_type=self.entity_description.permission_target,
|
||||
p_id=vmid,
|
||||
permission=self.entity_description.permission,
|
||||
):
|
||||
|
||||
@@ -21,7 +21,7 @@ VM_CONTAINER_RUNNING = "running"
|
||||
STORAGE_ACTIVE = 1
|
||||
STORAGE_SHARED = 1
|
||||
STORAGE_ENABLED = 1
|
||||
STATUS_OK = "ok"
|
||||
STATUS_OK = "OK"
|
||||
|
||||
AUTH_PAM = "pam"
|
||||
AUTH_PVE = "pve"
|
||||
@@ -41,3 +41,4 @@ class ProxmoxPermission(StrEnum):
|
||||
|
||||
POWER = "VM.PowerMgmt"
|
||||
SNAPSHOT = "VM.Snapshot"
|
||||
SYSPOWER = "Sys.PowerMgmt"
|
||||
|
||||
@@ -426,7 +426,11 @@ STORAGE_SENSORS: tuple[ProxmoxStorageSensorEntityDescription, ...] = (
|
||||
ProxmoxStorageSensorEntityDescription(
|
||||
key="storage_used_percentage",
|
||||
translation_key="storage_used_percentage",
|
||||
value_fn=lambda data: round(data["used_fraction"] * 100, 1),
|
||||
value_fn=lambda data: (
|
||||
round(value * 100, 1)
|
||||
if (value := data.get("used_fraction")) is not None
|
||||
else None
|
||||
),
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
"name": "Reset"
|
||||
},
|
||||
"shutdown": {
|
||||
"name": "Shutdown"
|
||||
"name": "Shut down"
|
||||
},
|
||||
"snapshot_create": {
|
||||
"name": "Create snapshot"
|
||||
@@ -313,7 +313,7 @@
|
||||
"message": "No active nodes were found on the Proxmox VE server."
|
||||
},
|
||||
"no_permission_node_power": {
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
|
||||
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'Sys.PowerMgmt' permission and try again."
|
||||
},
|
||||
"no_permission_snapshot": {
|
||||
"message": "The configured Proxmox VE user does not have permission to create snapshots of VMs and containers. Please grant the user the 'VM.Snapshot' permission and try again."
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
"codeowners": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/proxy",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["Pillow==12.1.1"]
|
||||
"requirements": ["Pillow==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiopvpc"],
|
||||
"requirements": ["aiopvpc==4.2.2"]
|
||||
"requirements": ["aiopvpc==4.3.1"]
|
||||
}
|
||||
|
||||
@@ -79,8 +79,10 @@ class QbusLight(QbusEntity, LightEntity):
|
||||
await self._async_publish_output_state(state)
|
||||
|
||||
async def _handle_state_received(self, state: QbusMqttAnalogState) -> None:
|
||||
percentage = round(state.read_percentage())
|
||||
self._set_state(percentage)
|
||||
percentage = state.read_percentage()
|
||||
|
||||
if percentage is not None:
|
||||
self._set_state(round(percentage))
|
||||
|
||||
def _set_state(self, percentage: int) -> None:
|
||||
self._attr_is_on = percentage > 0
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"cloudapp/QBUSMQTTGW/+/state"
|
||||
],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["qbusmqttapi==1.4.2"]
|
||||
"requirements": ["qbusmqttapi==1.4.3"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["pyzbar"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["Pillow==12.1.1", "pyzbar==0.1.7"]
|
||||
"requirements": ["Pillow==12.2.0", "pyzbar==0.1.7"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,14 @@ import voluptuous as vol
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_TYPE
|
||||
|
||||
from .const import CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, CONF_INSTALL_CODE, DOMAIN
|
||||
from .const import (
|
||||
CONF_CLOUD_ID,
|
||||
CONF_HARDWARE_ADDRESS,
|
||||
CONF_INSTALL_CODE,
|
||||
DOMAIN,
|
||||
TYPE_EAGLE_100,
|
||||
TYPE_EAGLE_200,
|
||||
)
|
||||
from .data import CannotConnect, InvalidAuth, async_get_type
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -63,11 +70,32 @@ class RainforestEagleConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CLOUD_ID], data=user_input
|
||||
)
|
||||
# Verify it is a known device, first
|
||||
if not eagle_type:
|
||||
errors["base"] = "unknown_device_type"
|
||||
elif eagle_type == TYPE_EAGLE_100:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
|
||||
# For EAGLE-100, there is no hardware address to select, so set it to None and move on
|
||||
user_input[CONF_HARDWARE_ADDRESS] = None
|
||||
elif eagle_type == TYPE_EAGLE_200:
|
||||
user_input[CONF_TYPE] = eagle_type
|
||||
|
||||
# For EAGLE-200, a connected meter's hardware address is required to create the entry
|
||||
if not hardware_address:
|
||||
# hardware_address will be None if there are no meters at all or if none are currently Connected
|
||||
errors["base"] = "no_meters_connected"
|
||||
else:
|
||||
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
|
||||
else:
|
||||
# This is a device that isn't supported, yet, but was detected by async_get_type
|
||||
errors["base"] = "unsupported_device_type"
|
||||
|
||||
# All information gathering is done, so if there are no errors at this point, create the entry
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_CLOUD_ID], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=create_schema(user_input), errors=errors
|
||||
|
||||
@@ -34,7 +34,7 @@ class InvalidAuth(RainforestError):
|
||||
|
||||
async def async_get_type(hass, cloud_id, install_code, host):
|
||||
"""Try API call 'get_network_info' to see if target device is Eagle-100 or Eagle-200."""
|
||||
# For EAGLE-200, fetch the hardware address of the meter too.
|
||||
# For EAGLE-200, fetch the hardware address of the first connected meter, too.
|
||||
hub = aioeagle.EagleHub(
|
||||
aiohttp_client.async_get_clientsession(hass), cloud_id, install_code, host=host
|
||||
)
|
||||
@@ -50,8 +50,17 @@ async def async_get_type(hass, cloud_id, install_code, host):
|
||||
|
||||
if meters is not None:
|
||||
if meters:
|
||||
hardware_address = meters[0].hardware_address
|
||||
# If there is at least one meter, use the first one with a connection status of "Connected"
|
||||
hardware_address = next(
|
||||
(
|
||||
m.hardware_address
|
||||
for m in meters
|
||||
if getattr(m, "connection_status", None) == "Connected"
|
||||
),
|
||||
None,
|
||||
)
|
||||
else:
|
||||
# If there are no meters (empty list, since None was already checked for), set the hardware address to None
|
||||
hardware_address = None
|
||||
|
||||
return TYPE_EAGLE_200, hardware_address
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"no_meters_connected": "No meters are currently connected. Ensure your meter is connected and try again.",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"unknown_device_type": "Unable to determine the type of Rainforest Eagle device. Please ensure your device is supported.",
|
||||
"unsupported_device_type": "This type of Rainforest Eagle device is not supported."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"services": {
|
||||
"disable": {
|
||||
"description": "Stops the recording of events and state changes.",
|
||||
"name": "[%key:common::action::disable%]"
|
||||
"name": "Disable Recorder"
|
||||
},
|
||||
"enable": {
|
||||
"description": "Starts the recording of events and state changes.",
|
||||
"name": "[%key:common::action::enable%]"
|
||||
"name": "Enable Recorder"
|
||||
},
|
||||
"get_statistics": {
|
||||
"description": "Retrieves statistics data for entities within a specific time period.",
|
||||
@@ -46,7 +46,7 @@
|
||||
"name": "Units"
|
||||
}
|
||||
},
|
||||
"name": "Get statistics"
|
||||
"name": "Get Recorder statistics"
|
||||
},
|
||||
"purge": {
|
||||
"description": "Starts purge task - to clean up old data from your database.",
|
||||
@@ -64,7 +64,7 @@
|
||||
"name": "Repack"
|
||||
}
|
||||
},
|
||||
"name": "Purge"
|
||||
"name": "Purge Recorder database"
|
||||
},
|
||||
"purge_entities": {
|
||||
"description": "Starts a purge task to remove the data related to specific entities from your database.",
|
||||
@@ -86,7 +86,7 @@
|
||||
"name": "[%key:component::recorder::services::purge::fields::keep_days::name%]"
|
||||
}
|
||||
},
|
||||
"name": "Purge entities"
|
||||
"name": "Purge Recorder entities"
|
||||
}
|
||||
},
|
||||
"system_health": {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyrisco"],
|
||||
"requirements": ["pyrisco==0.6.7"]
|
||||
"requirements": ["pyrisco==0.6.8"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,11 @@ from homeassistant.components.vacuum import (
|
||||
VacuumEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceResponse, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.exceptions import (
|
||||
HomeAssistantError,
|
||||
ServiceNotSupported,
|
||||
ServiceValidationError,
|
||||
)
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -484,6 +488,18 @@ class RoborockQ7Vacuum(RoborockCoordinatedEntityB01Q7, StateVacuumEntity):
|
||||
},
|
||||
) from err
|
||||
|
||||
async def get_maps(self) -> ServiceResponse:
|
||||
"""Get map information such as map id and room ids."""
|
||||
raise ServiceNotSupported(DOMAIN, "get_maps", self.entity_id)
|
||||
|
||||
async def get_vacuum_current_position(self) -> ServiceResponse:
|
||||
"""Get the current position of the vacuum from the map."""
|
||||
raise ServiceNotSupported(DOMAIN, "get_vacuum_current_position", self.entity_id)
|
||||
|
||||
async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
|
||||
"""Set the vacuum to go to a specific position."""
|
||||
raise ServiceNotSupported(DOMAIN, "set_vacuum_goto_position", self.entity_id)
|
||||
|
||||
|
||||
class RoborockQ10Vacuum(RoborockCoordinatedEntityB01Q10, StateVacuumEntity):
|
||||
"""Representation of a Roborock Q10 vacuum."""
|
||||
@@ -654,3 +670,15 @@ class RoborockQ10Vacuum(RoborockCoordinatedEntityB01Q10, StateVacuumEntity):
|
||||
"command": command,
|
||||
},
|
||||
) from err
|
||||
|
||||
async def get_maps(self) -> ServiceResponse:
|
||||
"""Get map information such as map id and room ids."""
|
||||
raise ServiceNotSupported(DOMAIN, "get_maps", self.entity_id)
|
||||
|
||||
async def get_vacuum_current_position(self) -> ServiceResponse:
|
||||
"""Get the current position of the vacuum from the map."""
|
||||
raise ServiceNotSupported(DOMAIN, "get_vacuum_current_position", self.entity_id)
|
||||
|
||||
async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
|
||||
"""Set the vacuum to go to a specific position."""
|
||||
raise ServiceNotSupported(DOMAIN, "set_vacuum_goto_position", self.entity_id)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["satel_integra"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["satel-integra==1.0.0"]
|
||||
"requirements": ["satel-integra==1.2.1"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ is_option_selected:
|
||||
required: true
|
||||
selector:
|
||||
state:
|
||||
attribute: options
|
||||
hide_states:
|
||||
- unavailable
|
||||
- unknown
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/seven_segments",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["Pillow==12.1.1"]
|
||||
"requirements": ["Pillow==12.2.0"]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -31,7 +31,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up 17Track from a config entry."""
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
session = async_create_clientsession(hass)
|
||||
client = SeventeenTrackClient(session=session)
|
||||
|
||||
try:
|
||||
|
||||
@@ -99,5 +99,5 @@ class SeventeenTrackConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@callback
|
||||
def _get_client(self):
|
||||
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||
session = aiohttp_client.async_create_clientsession(self.hass)
|
||||
return SeventeenTrackClient(session=session)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["simplehound"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["Pillow==12.1.1", "simplehound==0.3"]
|
||||
"requirements": ["Pillow==12.2.0", "simplehound==0.3"]
|
||||
}
|
||||
|
||||
@@ -38,5 +38,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pysmartthings"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pysmartthings==3.7.2"]
|
||||
"requirements": ["pysmartthings==3.7.3"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pysmhi"],
|
||||
"requirements": ["pysmhi==1.1.0"]
|
||||
"requirements": ["pysmhi==2.0.0"]
|
||||
}
|
||||
|
||||
@@ -56,6 +56,21 @@ FORESTDRY_MAP = {
|
||||
"5": "very_dry",
|
||||
"6": "extremely_dry",
|
||||
}
|
||||
PRECIPITATION_CATEGORY_MAP = {
|
||||
0: "no_precipitation",
|
||||
1: "rain",
|
||||
2: "thunderstorm",
|
||||
3: "freezing_rain",
|
||||
4: "mixed_ice",
|
||||
5: "snow",
|
||||
6: "wet_snow",
|
||||
7: "rain_snow_mixed",
|
||||
8: "ice_pellets",
|
||||
9: "graupel",
|
||||
10: "hail",
|
||||
11: "drizzle",
|
||||
12: "freezing_drizzle",
|
||||
}
|
||||
|
||||
|
||||
def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
|
||||
@@ -68,6 +83,14 @@ def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
|
||||
return None
|
||||
|
||||
|
||||
def get_precipitation_category(entity: SMHIWeatherSensor) -> str | None:
|
||||
"""Return the precipitation category."""
|
||||
value: int | None = entity.coordinator.current.get("precipitation_category")
|
||||
if value in PRECIPITATION_CATEGORY_MAP:
|
||||
return PRECIPITATION_CATEGORY_MAP[value]
|
||||
return None
|
||||
|
||||
|
||||
def get_fire_index_value(entity: SMHIFireSensor, key: str) -> str:
|
||||
"""Return index value as string."""
|
||||
value: int | None = entity.coordinator.fire_current.get(key) # type: ignore[assignment]
|
||||
@@ -128,11 +151,9 @@ WEATHER_SENSOR_DESCRIPTIONS: tuple[SMHIWeatherEntityDescription, ...] = (
|
||||
SMHIWeatherEntityDescription(
|
||||
key="precipitation_category",
|
||||
translation_key="precipitation_category",
|
||||
value_fn=lambda entity: str(
|
||||
get_percentage_values(entity, "precipitation_category")
|
||||
),
|
||||
value_fn=get_precipitation_category,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["0", "1", "2", "3", "4", "5", "6"],
|
||||
options=[*PRECIPITATION_CATEGORY_MAP.values()],
|
||||
),
|
||||
SMHIWeatherEntityDescription(
|
||||
key="frozen_precipitation",
|
||||
|
||||
@@ -95,13 +95,19 @@
|
||||
"precipitation_category": {
|
||||
"name": "Precipitation category",
|
||||
"state": {
|
||||
"0": "No precipitation",
|
||||
"1": "Snow",
|
||||
"2": "Snow and rain",
|
||||
"3": "Rain",
|
||||
"4": "Drizzle",
|
||||
"5": "Freezing rain",
|
||||
"6": "Freezing drizzle"
|
||||
"drizzle": "Drizzle",
|
||||
"freezing_drizzle": "Freezing drizzle",
|
||||
"freezing_rain": "Freezing rain",
|
||||
"graupel": "Graupel",
|
||||
"hail": "Hail",
|
||||
"ice_pellets": "Ice pellets",
|
||||
"mixed_ice": "Mixed/ice",
|
||||
"no_precipitation": "No precipitation",
|
||||
"rain": "Rain",
|
||||
"rain_snow_mixed": "Mixture of rain and snow",
|
||||
"snow": "Snow",
|
||||
"thunderstorm": "Thunderstorm",
|
||||
"wet_snow": "Wet snow"
|
||||
}
|
||||
},
|
||||
"rate_of_spread": {
|
||||
|
||||
@@ -417,6 +417,7 @@ class SonosDiscoveryManager:
|
||||
)
|
||||
new_coordinator.setup(soco)
|
||||
c_dict[soco.household_id] = new_coordinator
|
||||
c_dict[soco.household_id].add_speaker(soco)
|
||||
speaker.setup(self.entry)
|
||||
except (OSError, SoCoException, Timeout) as ex:
|
||||
_LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex)
|
||||
|
||||
@@ -99,3 +99,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
|
||||
)
|
||||
self.last_processed_event_id = self.alarms.last_id
|
||||
return True
|
||||
|
||||
def add_speaker(self, soco: SoCo) -> None:
|
||||
"""Update any skipped alarms when speaker is added."""
|
||||
self.alarms.update_skipped(soco)
|
||||
|
||||
@@ -85,3 +85,6 @@ class SonosHouseholdCoordinator:
|
||||
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
|
||||
"""Update the cache of the household-level feature and return if cache has changed."""
|
||||
raise NotImplementedError
|
||||
|
||||
def add_speaker(self, soco: SoCo) -> None:
|
||||
"""Additional processing when a speaker is added if needed."""
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user