Compare commits

...

45 Commits

Author SHA1 Message Date
Paulus Schoutsen a3e1209778 Bump version to 2026.5.0b1 2026-05-04 12:44:42 -04:00
Paul Bottein 7c44a0b88d Update frontend to 20260429.2 (#169748) 2026-05-04 12:44:23 -04:00
Manu 126058e0fa Bump bring-api to 1.1.2 (#169729) 2026-05-04 12:44:22 -04:00
Thomas D 28742822cb Ignore location FORBIDDEN response for the Volvo integration (#169713) 2026-05-04 12:44:21 -04:00
karwosts 179d370c2a Use uptime device_class for Uptime sensor (#169692) 2026-05-04 12:44:20 -04:00
Allen Porter 2d8f3691cf Update Nest doorbell event to use standard DoorbellEventType.RING (#169691) 2026-05-04 12:44:19 -04:00
Tom ce4fc9e880 Improve ProxmoxVE config flow preparing bug fixing (#169682)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-05-04 12:44:18 -04:00
Ronald van der Meer 9e357e7e5a Bump python-duco-client to 0.3.10 (#169677) 2026-05-04 12:44:17 -04:00
OMEGA_RAZER ed35b23e62 Updated prowlpy to 1.1.5 (#169671) 2026-05-04 12:44:17 -04:00
Tom Matheussen 191d2d1f12 Bump satel_integra to 1.3.0 (#169668) 2026-05-04 12:44:16 -04:00
SeifEddineMezned b165d8251f Fix grammar in mqtt/strings.json: "Minimal one" → "At least one" (#169666) 2026-05-04 12:44:15 -04:00
Midori Kochiya 5e8886aeb7 Fix M1S-T500 update error (#169651) 2026-05-04 12:44:14 -04:00
Michael bdb66635f8 Pass None config entry to schluter coordinator (#169621) 2026-05-04 12:44:13 -04:00
Michael 5ba6e348da Fix detection of CPU temperature sensor support on olde FRITZ!Box models (#169620) 2026-05-04 12:44:12 -04:00
Petro31 ed52b0ce80 Change vacuum template config names for clean area (#169599)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-05-04 12:44:11 -04:00
Jan-Philipp Benecke 33ee3d6967 Decrease WebDAV client timeout (#169591) 2026-05-04 12:44:10 -04:00
tronikos f36676c32c Bump opower to 0.18.2 (#169588) 2026-05-04 12:44:09 -04:00
Ronald van der Meer 77beddb1e7 Fix Duco unknown node type not re-evaluated after becoming known (#169579) 2026-05-04 12:42:31 -04:00
SeifEddineMezned 1677e410b3 Fix possessive apostrophe errors in mqtt/strings.json (#169576)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2026-05-04 12:38:37 -04:00
SeifEddineMezned 1be09347cd Fix grammar and clarity in samsungtv/strings.json (#169574) 2026-05-04 12:38:36 -04:00
Simone Chemelli c30ac2c0f3 Bump pyuptimerobot to 25.0.0 (#169572) 2026-05-04 12:37:45 -04:00
Shay Levy 145c7435a5 Bump aioshelly to 13.25.0 (#169569) 2026-05-04 12:36:21 -04:00
Paul Bottein 60f3b3bcc0 Update frontend to 20260429.1 (#169565) 2026-05-04 12:36:20 -04:00
Dan Raper 03e6d3bd30 Bump ohme to 1.9.0 (#169556) 2026-05-04 12:36:19 -04:00
Abílio Costa ee4d150e13 Use the correct schema for triggers/conditions "for" option (#169539) 2026-05-04 12:35:29 -04:00
bkobus-bbx 148603a10e Bump blebox_uniapi to 2.5.2 (#169534) 2026-05-04 12:33:13 -04:00
Erik Montnemery 1dbd933d3c Enable duration support in all entity conditions (#169532)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: frenck <195327+frenck@users.noreply.github.com>
2026-05-04 12:32:30 -04:00
Matthias Alphart f7ee7423fe Update knx-frontend to 2026.4.30.60856 (#169529) 2026-05-04 12:26:31 -04:00
Tomer 6322f1e37a Victron GX: Bug fix: parent device is mapped to the wrong device (#169525)
Co-authored-by: Copilot <copilot@github.com>
2026-05-04 12:26:30 -04:00
Manu 0d8c7fbb9d Fix: Migrate also device entries to subentry in GitHub integration (#169523) 2026-05-04 12:26:29 -04:00
Boris Bolshem 70e30b02a4 Fix KeyError in telegram_bot media group download debug log (#169519) 2026-05-04 12:26:28 -04:00
Simone Chemelli ebd21ea9b2 Fix uptime sensor for Synology DSM (#169512) 2026-05-04 12:26:27 -04:00
Erik Montnemery 9aa092cd34 Correct wake_on_lan entity behavior when entity_id changes (#169486)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-04 12:26:26 -04:00
TheJulianJES b274fe85b7 Re-interview ZHA device on websocket reconfigure (#169483) 2026-05-04 12:26:25 -04:00
Erik Montnemery 777c36998c Remove scripts from DATA_SCRIPTS on unload (#169415) 2026-05-04 12:26:24 -04:00
Kurt Chrisford a3977428f9 Implement current setpoint method in actron air integration (#169358) 2026-05-04 12:26:23 -04:00
Simone Chemelli 2d626c263c Storage problem management for Comelit Serial Bridge (#169297) 2026-05-04 12:26:22 -04:00
Jeef d1461f2e68 Bump weatherflow4py to 1.5.4 (#168994) 2026-05-04 12:26:21 -04:00
bkobus-bbx 3b778d2cc7 fix: incorrect position inversion for blebox gateBox cover (#168893) 2026-05-04 12:26:20 -04:00
Yuval Weiss 67b7d17a2f Add Broadlink infrared emitter support (#168889) 2026-05-04 12:26:18 -04:00
Tomer 1afeadc342 Victron GX: bug fix for missing translation key (#168461)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-04 12:26:17 -04:00
jftkcs f6aa4e2092 Fix reasoning summary handling for OpenAI o-models (#168093)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Denis Shulyaka <Shulyaka@gmail.com>
2026-05-04 12:26:16 -04:00
Khole 3b00c5bb96 Check device registration before completing Hive reauth flow (#168035)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-05-04 12:26:15 -04:00
Franck Nijhof ef7eed579b Bump version to 2026.5.0b0 2026-04-29 16:40:46 +00:00
Franck Nijhof 568a0085fe Bump version to 2026.5.0 2026-04-29 15:50:10 +00:00
169 changed files with 3027 additions and 651 deletions
@@ -147,7 +147,7 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
@property
def target_temperature(self) -> float:
"""Return the target temperature."""
return self._status.user_aircon_settings.temperature_setpoint_cool_c
return self._status.user_aircon_settings.current_setpoint
@actron_air_command
async def async_set_fan_mode(self, fan_mode: str) -> None:
@@ -239,7 +239,7 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
return self._zone.temperature_setpoint_cool_c
return self._zone.current_setpoint
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
@@ -36,9 +36,7 @@ def _make_detected_condition(
) -> type[Condition]:
"""Create a detected condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_ON,
support_duration=True,
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_ON
)
@@ -47,9 +45,7 @@ def _make_cleared_condition(
) -> type[Condition]:
"""Create a cleared condition for a binary sensor device class."""
return make_entity_state_condition(
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)},
STATE_OFF,
support_duration=True,
{BINARY_SENSOR_DOMAIN: DomainSpec(device_class=device_class)}, STATE_OFF
)
@@ -7,6 +7,12 @@
automation_behavior:
mode: condition
.condition_for: &condition_for
required: true
default: 00:00:00
selector:
duration:
# --- Unit lists for multi-unit pollutants ---
.co_units: &co_units
@@ -246,11 +252,7 @@
.condition_binary_common: &condition_binary_common
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
for: *condition_for
is_gas_detected:
<<: *condition_binary_common
@@ -282,6 +284,7 @@ is_co_value:
target: *target_co_sensor
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -296,6 +299,7 @@ is_ozone_value:
target: *target_ozone
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -310,6 +314,7 @@ is_voc_value:
target: *target_voc
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -324,6 +329,7 @@ is_voc_ratio_value:
target: *target_voc_ratio
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -338,6 +344,7 @@ is_no_value:
target: *target_no
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -352,6 +359,7 @@ is_no2_value:
target: *target_no2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -366,6 +374,7 @@ is_so2_value:
target: *target_so2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -382,6 +391,7 @@ is_co2_value:
target: *target_co2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -394,6 +404,7 @@ is_pm1_value:
target: *target_pm1
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -406,6 +417,7 @@ is_pm25_value:
target: *target_pm25
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -418,6 +430,7 @@ is_pm4_value:
target: *target_pm4
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -430,6 +443,7 @@ is_pm10_value:
target: *target_pm10
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -442,6 +456,7 @@ is_n2o_value:
target: *target_n2o
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -14,6 +14,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -50,6 +53,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -86,6 +92,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -98,6 +107,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -110,6 +122,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -122,6 +137,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -134,6 +152,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -146,6 +167,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -158,6 +182,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -170,6 +197,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -206,6 +236,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -218,6 +251,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -230,6 +266,9 @@
"behavior": {
"name": "[%key:component::air_quality::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::air_quality::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::air_quality::common::condition_threshold_name%]"
}
@@ -4,7 +4,6 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
Condition,
EntityStateConditionBase,
make_entity_state_condition,
@@ -26,7 +25,6 @@ class EntityStateRequiredFeaturesCondition(EntityStateConditionBase):
"""State condition."""
_required_features: int
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
def entity_filter(self, entities: set[str]) -> set[str]:
"""Filter entities of this domain with the required features."""
@@ -84,11 +82,9 @@ CONDITIONS: dict[str, type[Condition]] = {
AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION,
),
"is_disarmed": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.DISARMED, support_duration=True
),
"is_disarmed": make_entity_state_condition(DOMAIN, AlarmControlPanelState.DISARMED),
"is_triggered": make_entity_state_condition(
DOMAIN, AlarmControlPanelState.TRIGGERED, support_duration=True
DOMAIN, AlarmControlPanelState.TRIGGERED
),
}
@@ -1,19 +1,14 @@
.condition_common: &condition_common
target: &condition_common_target
target:
entity:
domain: alarm_control_panel
fields: &condition_common_fields
behavior: &condition_common_behavior
behavior:
required: true
default: any
selector:
automation_behavior:
mode: condition
.condition_common_for: &condition_common_for
target: *condition_common_target
fields: &condition_common_for_fields
behavior: *condition_common_behavior
for:
required: true
default: 00:00:00
@@ -23,7 +18,7 @@
is_armed: *condition_common
is_armed_away:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -31,7 +26,7 @@ is_armed_away:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_AWAY
is_armed_home:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -39,7 +34,7 @@ is_armed_home:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_HOME
is_armed_night:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
@@ -47,13 +42,13 @@ is_armed_night:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_NIGHT
is_armed_vacation:
fields: *condition_common_for_fields
fields: *condition_common_fields
target:
entity:
domain: alarm_control_panel
supported_features:
- alarm_control_panel.AlarmControlPanelEntityFeature.ARM_VACATION
is_disarmed: *condition_common_for
is_disarmed: *condition_common
is_triggered: *condition_common_for
is_triggered: *condition_common
@@ -11,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::alarm_control_panel::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::alarm_control_panel::common::condition_for_name%]"
}
},
"name": "Alarm is armed"
@@ -7,17 +7,13 @@ from .const import DOMAIN
from .entity import AssistSatelliteState
CONDITIONS: dict[str, type[Condition]] = {
"is_idle": make_entity_state_condition(
DOMAIN, AssistSatelliteState.IDLE, support_duration=True
),
"is_listening": make_entity_state_condition(
DOMAIN, AssistSatelliteState.LISTENING, support_duration=True
),
"is_idle": make_entity_state_condition(DOMAIN, AssistSatelliteState.IDLE),
"is_listening": make_entity_state_condition(DOMAIN, AssistSatelliteState.LISTENING),
"is_processing": make_entity_state_condition(
DOMAIN, AssistSatelliteState.PROCESSING, support_duration=True
DOMAIN, AssistSatelliteState.PROCESSING
),
"is_responding": make_entity_state_condition(
DOMAIN, AssistSatelliteState.RESPONDING, support_duration=True
DOMAIN, AssistSatelliteState.RESPONDING
),
}
@@ -32,25 +32,21 @@ CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_ON,
support_duration=True,
primary_entities_only=False,
),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_OFF,
support_duration=True,
primary_entities_only=False,
),
"is_level": make_entity_numerical_condition(
@@ -63,6 +63,7 @@ is_level:
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -26,6 +26,9 @@
"behavior": {
"name": "[%key:component::battery::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::battery::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::battery::common::condition_threshold_name%]"
}
+3 -1
View File
@@ -85,7 +85,9 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
if position == -1: # possible for shutterBox
return None
return None if position is None else 100 - position
if position is None:
return None
return 100 - position if self._feature.is_position_inverted else position
@property
def current_cover_tilt_position(self) -> int | None:
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.1"],
"requirements": ["blebox-uniapi==2.5.2"],
"zeroconf": ["_bbxsrv._tcp.local."]
}
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["bring_api"],
"quality_scale": "platinum",
"requirements": ["bring-api==1.1.1"]
"requirements": ["bring-api==1.1.2"]
}
@@ -6,6 +6,7 @@ DOMAIN = "broadlink"
DOMAINS_AND_TYPES = {
Platform.CLIMATE: {"HYS"},
Platform.INFRARED: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
Platform.LIGHT: {"LB1", "LB2"},
Platform.RADIO_FREQUENCY: {"RM4PRO", "RMPRO"},
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
@@ -0,0 +1,69 @@
"""Infrared platform for Broadlink remotes."""
from __future__ import annotations
from typing import TYPE_CHECKING
from broadlink.exceptions import BroadlinkException
from broadlink.remote import pulses_to_data as _bl_pulses_to_data
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import BroadlinkEntity
if TYPE_CHECKING:
from .device import BroadlinkDevice
PARALLEL_UPDATES = 1
def _timings_to_broadlink_packet(timings: list[int]) -> bytes:
"""Convert signed microsecond timings to a Broadlink IR packet.
Positive values are pulse (high) durations; negative values are space
(low) durations. The Broadlink library's encoder expects absolute
durations.
"""
pulses = [abs(t) for t in timings]
return _bl_pulses_to_data(pulses)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Broadlink infrared entity."""
# Uses legacy hass.data[DOMAIN] pattern
# pylint: disable-next=hass-use-runtime-data
device = hass.data[DOMAIN].devices[config_entry.entry_id]
async_add_entities([BroadlinkInfraredEntity(device)])
class BroadlinkInfraredEntity(BroadlinkEntity, InfraredEntity):
"""Broadlink infrared transmitter entity."""
_attr_has_entity_name = True
_attr_translation_key = "infrared_emitter"
def __init__(self, device: BroadlinkDevice) -> None:
"""Initialize the entity."""
super().__init__(device)
self._attr_unique_id = f"{device.unique_id}-emitter"
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command via the Broadlink device."""
packet = _timings_to_broadlink_packet(command.get_raw_timings())
try:
await self._device.async_request(self._device.api.send_data, packet)
except (BroadlinkException, OSError) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="send_command_failed",
translation_placeholders={"error": str(err)},
) from err
@@ -49,6 +49,11 @@
}
},
"entity": {
"infrared": {
"infrared_emitter": {
"name": "IR emitter"
}
},
"select": {
"day_of_week": {
"name": "Day of week",
@@ -82,6 +87,9 @@
"frequency_not_supported": {
"message": "Broadlink devices cannot transmit on {frequency} MHz"
},
"send_command_failed": {
"message": "Failed to send IR command: {error}"
},
"transmit_failed": {
"message": "Failed to transmit RF command: {error}"
}
@@ -7,9 +7,7 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_event_active": make_entity_state_condition(
DOMAIN, STATE_ON, support_duration=True
),
"is_event_active": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -67,7 +67,7 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_hvac_mode": ClimateHVACModeCondition,
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, HVACMode.OFF),
"is_on": make_entity_state_condition(
DOMAIN,
{
@@ -9,6 +9,11 @@
selector:
automation_behavior:
mode: condition
for: &condition_for
required: true
default: 00:00:00
selector:
duration:
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -36,16 +41,7 @@
- domain: number
device_class: temperature
is_off:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_off: *condition_common
is_on: *condition_common
is_cooling: *condition_common
is_drying: *condition_common
@@ -55,6 +51,7 @@ is_hvac_mode:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
hvac_mode:
context:
filter_target: target
@@ -70,6 +67,7 @@ target_humidity:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -82,6 +80,7 @@ target_temperature:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -13,6 +13,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Thermostat is cooling"
@@ -22,6 +25,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Thermostat is drying"
@@ -31,6 +37,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Thermostat is heating"
@@ -41,6 +50,9 @@
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
},
"hvac_mode": {
"description": "The HVAC modes to test for.",
"name": "Modes"
@@ -65,6 +77,9 @@
"fields": {
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
}
},
"name": "Thermostat is on"
@@ -75,6 +90,9 @@
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
@@ -87,6 +105,9 @@
"behavior": {
"name": "[%key:component::climate::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::climate::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::climate::common::condition_threshold_name%]"
}
@@ -18,7 +18,12 @@ from aiocomelit.const import (
SCENARIO,
VEDO,
)
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiocomelit.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
DeviceStorageFailureError,
)
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
@@ -112,6 +117,11 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
) from err
except DeviceStorageFailureError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="device_storage_failure",
) from err
@abstractmethod
async def _async_update_system_data(self) -> T:
@@ -121,6 +121,9 @@
"cannot_retrieve_data": {
"message": "Error retrieving data: {error}"
},
"device_storage_failure": {
"message": "Device SD card read failure. The card may be corrupted or failing; replacement is recommended."
},
"humidity_while_off": {
"message": "Cannot change humidity while off"
},
+12 -1
View File
@@ -5,7 +5,12 @@ from functools import wraps
from typing import TYPE_CHECKING, Any, Concatenate, Literal
from aiocomelit.api import ComelitSerialBridgeObject
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiocomelit.exceptions import (
CannotAuthenticate,
CannotConnect,
CannotRetrieveData,
DeviceStorageFailureError,
)
from aiohttp import ClientSession, CookieJar
from homeassistant.config_entries import ConfigEntry
@@ -110,6 +115,12 @@ def bridge_api_call[_T: ComelitBridgeBaseEntity, **_P](
translation_key="cannot_retrieve_data",
translation_placeholders={"error": repr(err)},
) from err
except DeviceStorageFailureError as err:
self.coordinator.last_update_success = False
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="device_storage_failure",
) from err
except CannotAuthenticate:
self.coordinator.last_update_success = False
self.coordinator.config_entry.async_start_reauth(self.hass)
@@ -9,6 +9,11 @@ is_value:
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
threshold:
required: true
selector:
@@ -1,5 +1,6 @@
{
"common": {
"condition_for_name": "For at least",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
},
@@ -10,6 +11,9 @@
"behavior": {
"name": "Condition passes if"
},
"for": {
"name": "[%key:component::counter::common::condition_for_name%]"
},
"threshold": {
"name": "Threshold type"
}
+1 -6
View File
@@ -4,11 +4,7 @@ from collections.abc import Mapping
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR,
Condition,
EntityConditionBase,
)
from homeassistant.helpers.condition import Condition, EntityConditionBase
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
from .models import CoverDomainSpec
@@ -18,7 +14,6 @@ class CoverConditionBase(EntityConditionBase):
"""Base condition for cover state checks."""
_domain_specs: Mapping[str, CoverDomainSpec]
_schema = ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL_FOR
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches the expected cover state."""
+1 -1
View File
@@ -13,7 +13,7 @@
"iot_class": "local_polling",
"loggers": ["duco"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.9"],
"requirements": ["python-duco-client==0.3.10"],
"zeroconf": [
{
"name": "duco [[][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][]].*",
+9 -3
View File
@@ -143,6 +143,7 @@ async def async_setup_entry(
@callback
def _async_add_new_entities() -> None:
"""Add new sensor entities and remove stale ones on coordinator updates."""
# Remove devices whose nodes have disappeared from the API.
# The firmware removes deregistered RF/wired nodes automatically.
# BSRH box sensors that are physically unplugged from the PCB are
@@ -166,14 +167,19 @@ async def async_setup_entry(
for node in coordinator.data.nodes.values():
if node.node_id in known_nodes:
continue
known_nodes.add(node.node_id)
if node.general.node_type == NodeType.UNKNOWN:
_LOGGER.warning(
"Duco node %s (%s) has an unsupported device type and will be ignored",
# Do not add the node to known_nodes so that it is re-evaluated
# on every coordinator update. This allows entities to be
# created automatically once a firmware update or library
# update adds support for the device type.
_LOGGER.debug(
"Duco node %s (%s) has an unsupported device type and will be "
"retried on subsequent coordinator updates",
node.node_id,
node.general.name,
)
continue
known_nodes.add(node.node_id)
new_entities.extend(
DucoSensorEntity(coordinator, node, description)
for description in SENSOR_DESCRIPTIONS
+2 -2
View File
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
+2 -1
View File
@@ -7,6 +7,7 @@ from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from fritzconnection.core.exceptions import FritzConnectionException
from fritzconnection.lib.fritzstatus import FritzStatus
from requests.exceptions import RequestException
@@ -145,7 +146,7 @@ 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:
except RequestException, IndexError, FritzConnectionException:
_LOGGER.debug("CPU temperature not supported by the device")
return False
if cpu_temp == 0:
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260429.0"]
"requirements": ["home-assistant-frontend==20260429.2"]
}
+10 -3
View File
@@ -9,12 +9,13 @@ from aiogithubapi import GitHubAPI
from homeassistant.config_entries import ConfigSubentry
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import (
SERVER_SOFTWARE,
async_get_clientsession,
)
from .const import CONF_REPOSITORIES, CONF_REPOSITORY, SUBENTRY_TYPE_REPOSITORY
from .const import CONF_REPOSITORIES, CONF_REPOSITORY, DOMAIN, SUBENTRY_TYPE_REPOSITORY
from .coordinator import GithubConfigEntry, GitHubDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.SENSOR]
@@ -68,6 +69,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> b
async def async_migrate_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bool:
"""Migrate old entry."""
if entry.minor_version == 1:
dev_reg = dr.async_get(hass)
# In minor version 2 we migrated repositories from entry options to
# subentries, so we need to convert the list from
# entry.options[CONF_REPOSITORIES] into individual subentries.
@@ -78,8 +80,13 @@ async def async_migrate_entry(hass: HomeAssistant, entry: GithubConfigEntry) ->
title=repository,
unique_id=repository,
)
hass.config_entries.async_add_subentry(entry, subentry)
if device := dev_reg.async_get_device({(DOMAIN, repository)}):
dev_reg.async_update_device(
device.id,
remove_config_entry_id=entry.entry_id,
add_config_subentry_id=subentry.subentry_id,
add_config_entry_id=entry.entry_id,
)
hass.config_entries.async_update_entry(entry, minor_version=2)
return True
+17 -3
View File
@@ -119,9 +119,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
if not errors:
_LOGGER.debug("2FA successful")
if self.source == SOURCE_REAUTH:
return await self.async_setup_hive_entry()
self.device_registration = True
return await self.async_step_configuration()
try:
device_registered = await self.hive_auth.is_device_registered()
except HiveApiError as err:
_LOGGER.debug(
"Failed to check whether the Hive device is registered during reauthentication: %s",
err,
)
errors["base"] = "no_internet_available"
else:
if device_registered:
return await self.async_setup_hive_entry()
self.device_registration = True
return await self.async_step_configuration()
else:
self.device_registration = True
return await self.async_step_configuration()
schema = vol.Schema({vol.Required(CONF_CODE): str})
return self.async_show_form(step_id="2fa", data_schema=schema, errors=errors)
@@ -173,6 +186,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Re Authenticate a user."""
self.data = dict(entry_data)
data = {
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
@@ -70,8 +70,8 @@ class IsModeCondition(EntityStateConditionBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
"is_drying": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.DRYING
),
@@ -9,12 +9,7 @@
selector:
automation_behavior:
mode: condition
.condition_common_for: &condition_common_for
target: *condition_humidifier_target
fields:
behavior: *condition_behavior
for:
for: &condition_for
required: true
default: 00:00:00
selector:
@@ -34,8 +29,8 @@
mode: box
unit_of_measurement: "%"
is_off: *condition_common_for
is_on: *condition_common_for
is_off: *condition_common
is_on: *condition_common
is_drying: *condition_common
is_humidifying: *condition_common
@@ -43,6 +38,7 @@ is_mode:
target: *condition_humidifier_target
fields:
behavior: *condition_behavior
for: *condition_for
mode:
context:
filter_target: target
@@ -56,6 +52,7 @@ is_target_humidity:
target: *condition_humidifier_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -12,6 +12,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::condition_for_name%]"
}
},
"name": "Humidifier is drying"
@@ -21,6 +24,9 @@
"fields": {
"behavior": {
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::condition_for_name%]"
}
},
"name": "Humidifier is humidifying"
@@ -31,6 +37,9 @@
"behavior": {
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::condition_for_name%]"
},
"mode": {
"description": "The operation modes to check for.",
"name": "Mode"
@@ -68,6 +77,9 @@
"behavior": {
"name": "[%key:component::humidifier::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::humidifier::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::humidifier::common::condition_threshold_name%]"
}
@@ -27,6 +27,11 @@ is_value:
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
threshold:
required: true
selector:
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -13,6 +14,9 @@
"behavior": {
"name": "[%key:component::humidity::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::humidity::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::humidity::common::condition_threshold_name%]"
}
@@ -25,10 +25,10 @@ ILLUMINANCE_VALUE_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_ON, support_duration=True
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_ON
),
"is_not_detected": make_entity_state_condition(
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_OFF, support_duration=True
ILLUMINANCE_DETECTED_DOMAIN_SPECS, STATE_OFF
),
"is_value": make_entity_numerical_condition(
ILLUMINANCE_VALUE_DOMAIN_SPECS, LIGHT_LUX
@@ -10,7 +10,7 @@
selector:
automation_behavior:
mode: condition
for:
for: &condition_for
required: true
default: 00:00:00
selector:
@@ -27,6 +27,7 @@ is_value:
device_class: illuminance
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -38,6 +38,9 @@
"behavior": {
"name": "[%key:component::illuminance::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::illuminance::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::illuminance::common::condition_threshold_name%]"
}
+1 -1
View File
@@ -13,7 +13,7 @@
"requirements": [
"xknx==3.15.0",
"xknxproject==3.8.2",
"knx-frontend==2026.4.25.155016"
"knx-frontend==2026.4.30.60856"
],
"single_config_entry": true
}
@@ -6,21 +6,13 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN, LawnMowerActivity
CONDITIONS: dict[str, type[Condition]] = {
"is_docked": make_entity_state_condition(
DOMAIN, LawnMowerActivity.DOCKED, support_duration=True
),
"is_docked": make_entity_state_condition(DOMAIN, LawnMowerActivity.DOCKED),
"is_encountering_an_error": make_entity_state_condition(
DOMAIN, LawnMowerActivity.ERROR, support_duration=True
),
"is_mowing": make_entity_state_condition(
DOMAIN, LawnMowerActivity.MOWING, support_duration=True
),
"is_paused": make_entity_state_condition(
DOMAIN, LawnMowerActivity.PAUSED, support_duration=True
),
"is_returning": make_entity_state_condition(
DOMAIN, LawnMowerActivity.RETURNING, support_duration=True
DOMAIN, LawnMowerActivity.ERROR
),
"is_mowing": make_entity_state_condition(DOMAIN, LawnMowerActivity.MOWING),
"is_paused": make_entity_state_condition(DOMAIN, LawnMowerActivity.PAUSED),
"is_returning": make_entity_state_condition(DOMAIN, LawnMowerActivity.RETURNING),
}
+2 -2
View File
@@ -38,8 +38,8 @@ class BrightnessCondition(EntityNumericalConditionBase):
CONDITIONS: dict[str, type[Condition]] = {
"is_brightness": BrightnessCondition,
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -9,7 +9,7 @@
selector:
automation_behavior:
mode: condition
for:
for: &condition_for
required: true
default: 00:00:00
selector:
@@ -36,6 +36,7 @@ is_brightness:
target: *condition_light_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -47,6 +47,9 @@
"behavior": {
"name": "[%key:component::light::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::light::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::light::common::condition_threshold_name%]"
}
+4 -12
View File
@@ -6,18 +6,10 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN, LockState
CONDITIONS: dict[str, type[Condition]] = {
"is_jammed": make_entity_state_condition(
DOMAIN, LockState.JAMMED, support_duration=True
),
"is_locked": make_entity_state_condition(
DOMAIN, LockState.LOCKED, support_duration=True
),
"is_open": make_entity_state_condition(
DOMAIN, LockState.OPEN, support_duration=True
),
"is_unlocked": make_entity_state_condition(
DOMAIN, LockState.UNLOCKED, support_duration=True
),
"is_jammed": make_entity_state_condition(DOMAIN, LockState.JAMMED),
"is_locked": make_entity_state_condition(DOMAIN, LockState.LOCKED),
"is_open": make_entity_state_condition(DOMAIN, LockState.OPEN),
"is_unlocked": make_entity_state_condition(DOMAIN, LockState.UNLOCKED),
}
@@ -6,9 +6,7 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN, MediaPlayerState
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(
DOMAIN, MediaPlayerState.OFF, support_duration=True
),
"is_off": make_entity_state_condition(DOMAIN, MediaPlayerState.OFF),
"is_on": make_entity_state_condition(
DOMAIN,
{
@@ -29,12 +27,8 @@ CONDITIONS: dict[str, type[Condition]] = {
MediaPlayerState.PAUSED,
},
),
"is_paused": make_entity_state_condition(
DOMAIN, MediaPlayerState.PAUSED, support_duration=True
),
"is_playing": make_entity_state_condition(
DOMAIN, MediaPlayerState.PLAYING, support_duration=True
),
"is_paused": make_entity_state_condition(DOMAIN, MediaPlayerState.PAUSED),
"is_playing": make_entity_state_condition(DOMAIN, MediaPlayerState.PLAYING),
}
@@ -1,27 +1,22 @@
.condition_common: &condition_common
target: &condition_media_player_target
target:
entity:
domain: media_player
fields:
behavior: &condition_behavior
behavior:
required: true
default: any
selector:
automation_behavior:
mode: condition
.condition_common_with_for: &condition_common_with_for
target: *condition_media_player_target
fields:
behavior: *condition_behavior
for:
required: true
default: 00:00:00
selector:
duration:
is_off: *condition_common_with_for
is_off: *condition_common
is_on: *condition_common
is_not_playing: *condition_common
is_paused: *condition_common_with_for
is_playing: *condition_common_with_for
is_paused: *condition_common
is_playing: *condition_common
@@ -11,6 +11,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::condition_for_name%]"
}
},
"name": "Media player is not playing"
@@ -32,6 +35,9 @@
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::condition_for_name%]"
}
},
"name": "Media player is on"
@@ -27,11 +27,9 @@ _MOISTURE_NUMERICAL_DOMAIN_SPECS = {
}
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_detected": make_entity_state_condition(_MOISTURE_BINARY_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_OFF, support_duration=True
_MOISTURE_BINARY_DOMAIN_SPECS, STATE_OFF
),
"is_value": make_entity_numerical_condition(
_MOISTURE_NUMERICAL_DOMAIN_SPECS, PERCENTAGE
@@ -10,7 +10,7 @@
selector:
automation_behavior:
mode: condition
for:
for: &condition_for
required: true
default: 00:00:00
selector:
@@ -41,6 +41,7 @@ is_value:
device_class: moisture
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -38,6 +38,9 @@
"behavior": {
"name": "[%key:component::moisture::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::moisture::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::moisture::common::condition_threshold_name%]"
}
+2 -6
View File
@@ -15,12 +15,8 @@ _MOTION_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(
_MOTION_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_detected": make_entity_state_condition(
_MOTION_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(_MOTION_DOMAIN_SPECS, STATE_OFF),
}
+4 -4
View File
@@ -48,7 +48,7 @@
"data_description": {
"advanced_options": "Enable and select **Submit** to set advanced options.",
"broker": "The hostname or IP address of your MQTT broker.",
"certificate": "The custom CA certificate file to validate your MQTT brokers certificate.",
"certificate": "The custom CA certificate file to validate your MQTT broker's certificate.",
"client_cert": "The client certificate to authenticate against your MQTT broker.",
"client_id": "The unique ID to identify the Home Assistant MQTT API as MQTT client. It is recommended to leave this option blank.",
"client_key": "The private key file that belongs to your client certificate.",
@@ -57,7 +57,7 @@
"password": "The password to log in to your MQTT broker.",
"port": "The port your MQTT broker listens to. For example 1883.",
"protocol": "The MQTT protocol your broker operates at. For example 3.1.1.",
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT brokers certificate.",
"set_ca_cert": "Select **Auto** for automatic CA validation, or **Custom** and select **Next** to set a custom CA certificate, to allow validating your MQTT broker's certificate.",
"set_client_cert": "Enable and select **Next** to set a client certificate and private key to authenticate against your MQTT broker.",
"tls_insecure": "Option to ignore validation of your MQTT broker's certificate.",
"transport": "The transport to be used for the connection to your MQTT broker.",
@@ -83,7 +83,7 @@
"password": "[%key:component::mqtt::config::step::broker::data_description::password%]",
"username": "[%key:component::mqtt::config::step::broker::data_description::username%]"
},
"description": "The MQTT broker reported an authentication error. Please confirm the brokers correct username and password.",
"description": "The MQTT broker reported an authentication error. Please confirm the broker's correct username and password.",
"title": "Re-authentication required with the MQTT broker"
},
"start_addon": {
@@ -162,7 +162,7 @@
"component": "Entity"
},
"data_description": {
"component": "Select the entity you want to delete. Minimal one entity is required."
"component": "Select the entity you want to delete. At least one entity is required."
},
"description": "Delete an entity. The entity will be removed from the device. Removing an entity will break any automations or scripts that depend on it.",
"title": "Delete entity"
+6 -2
View File
@@ -8,6 +8,7 @@ from google_nest_sdm.event import EventMessage, EventType
from google_nest_sdm.traits import TraitType
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -42,7 +43,7 @@ ENTITY_DESCRIPTIONS = [
key=EVENT_DOORBELL_CHIME,
translation_key="chime",
device_class=EventDeviceClass.DOORBELL,
event_types=[EVENT_DOORBELL_CHIME],
event_types=[DoorbellEventType.RING],
trait_types=[TraitType.DOORBELL_CHIME],
api_event_types=[EventType.DOORBELL_CHIME],
),
@@ -80,7 +81,7 @@ async def async_setup_entry(
class NestTraitEventEntity(EventEntity):
"""Nest doorbell event entity."""
"""Nest event entity for event entity descriptions."""
entity_description: NestEventEntityDescription
_attr_has_entity_name = True
@@ -113,6 +114,9 @@ class NestTraitEventEntity(EventEntity):
# This event is a duplicate message in the same thread
return
if event_type == EVENT_DOORBELL_CHIME:
event_type = DoorbellEventType.RING
self._trigger_event(
event_type,
{"nest_event_id": nest_event_id},
+1 -1
View File
@@ -113,7 +113,7 @@
"state_attributes": {
"event_type": {
"state": {
"doorbell_chime": "[%key:component::nest::entity::event::chime::name%]"
"ring": "[%key:component::event::entity_component::doorbell::state_attributes::event_type::state::ring%]"
}
}
}
@@ -15,12 +15,8 @@ _OCCUPANCY_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_detected": make_entity_state_condition(
_OCCUPANCY_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_not_detected": make_entity_state_condition(
_OCCUPANCY_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_ON),
"is_not_detected": make_entity_state_condition(_OCCUPANCY_DOMAIN_SPECS, STATE_OFF),
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.7.1"]
"requirements": ["ohme==1.9.0"]
}
@@ -46,6 +46,7 @@ from .const import (
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_REASONING_EFFORT,
CONF_REASONING_SUMMARY,
CONF_STORE_RESPONSES,
CONF_TEMPERATURE,
CONF_TOP_P,
@@ -59,6 +60,7 @@ from .const import (
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_REASONING_SUMMARY,
RECOMMENDED_STORE_RESPONSES,
RECOMMENDED_STT_OPTIONS,
RECOMMENDED_TEMPERATURE,
@@ -490,6 +492,25 @@ async def async_migrate_entry(hass: HomeAssistant, entry: OpenAIConfigEntry) ->
_add_stt_subentry(hass, entry)
hass.config_entries.async_update_entry(entry, minor_version=6)
if entry.version == 2 and entry.minor_version == 6:
for subentry in entry.subentries.values():
if subentry.subentry_type in ("conversation", "ai_task_data"):
data = dict(subentry.data)
updated = False
if data.get(CONF_REASONING_SUMMARY) == "short":
data[CONF_REASONING_SUMMARY] = "concise"
updated = True
if data.get(CONF_REASONING_SUMMARY) == "concise" and not data.get(
CONF_CHAT_MODEL, ""
).startswith("gpt-5"):
data[CONF_REASONING_SUMMARY] = RECOMMENDED_REASONING_SUMMARY
updated = True
if updated:
hass.config_entries.async_update_subentry(
entry, subentry, data=data
)
hass.config_entries.async_update_entry(entry, minor_version=7)
LOGGER.debug(
"Migration to version %s:%s successful", entry.version, entry.minor_version
)
@@ -127,7 +127,7 @@ class OpenAIConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OpenAI Conversation."""
VERSION = 2
MINOR_VERSION = 6
MINOR_VERSION = 7
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -435,23 +435,37 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
elif CONF_VERBOSITY in options:
options.pop(CONF_VERBOSITY)
if model.startswith(("o", "gpt-5")):
reasoning_summary_options = ["off", "auto", "concise", "detailed"]
if model.startswith("o"):
reasoning_summary_options.remove("concise")
stored_summary = options.get(
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
)
if stored_summary not in reasoning_summary_options:
stored_summary = RECOMMENDED_REASONING_SUMMARY
options[CONF_REASONING_SUMMARY] = stored_summary
step_schema.update(
{
vol.Optional(
CONF_REASONING_SUMMARY,
default=RECOMMENDED_REASONING_SUMMARY,
default=stored_summary,
): SelectSelector(
SelectSelectorConfig(
options=["off", "auto", "short", "detailed"],
options=reasoning_summary_options,
translation_key=CONF_REASONING_SUMMARY,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
)
elif CONF_VERBOSITY in options:
options.pop(CONF_VERBOSITY)
if CONF_REASONING_SUMMARY in options:
if not model.startswith("gpt-5"):
options.pop(CONF_REASONING_SUMMARY)
elif CONF_REASONING_SUMMARY in options:
options.pop(CONF_REASONING_SUMMARY)
service_tiers = self._get_service_tiers(model)
if "flex" in service_tiers or "priority" in service_tiers:
@@ -43,7 +43,10 @@ from openai.types.responses import (
ToolParam,
WebSearchToolParam,
)
from openai.types.responses.response_create_params import ResponseCreateParamsStreaming
from openai.types.responses.response_create_params import (
Reasoning,
ResponseCreateParamsStreaming,
)
from openai.types.responses.response_input_param import (
FunctionCallOutput,
ImageGenerationCall as ImageGenerationCallParam,
@@ -520,16 +523,19 @@ class OpenAIBaseLLMEntity(Entity):
)
if model_args["model"].startswith(("o", "gpt-5")):
model_args["reasoning"] = {
reasoning: Reasoning = {
"effort": options.get(
CONF_REASONING_EFFORT, RECOMMENDED_REASONING_EFFORT
)
if not model_args["model"].startswith("gpt-5-pro")
else "high", # GPT-5 pro only supports reasoning.effort: high
"summary": options.get(
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
),
}
reasoning_summary = options.get(
CONF_REASONING_SUMMARY, RECOMMENDED_REASONING_SUMMARY
)
if reasoning_summary != "off":
reasoning["summary"] = reasoning_summary
model_args["reasoning"] = reasoning
model_args["include"] = ["reasoning.encrypted_content"]
if (
@@ -242,9 +242,9 @@
"reasoning_summary": {
"options": {
"auto": "[%key:common::state::auto%]",
"concise": "Concise",
"detailed": "Detailed",
"off": "[%key:common::state::off%]",
"short": "Short"
"off": "[%key:common::state::off%]"
}
},
"search_context_size": {
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "platinum",
"requirements": ["opower==0.18.1"]
"requirements": ["opower==0.18.2"]
}
@@ -5,6 +5,12 @@
automation_behavior:
mode: condition
.condition_for: &condition_for
required: true
default: 00:00:00
selector:
duration:
.power_units: &power_units
- "mW"
- "W"
@@ -29,6 +35,7 @@ is_value:
device_class: power
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -13,6 +14,9 @@
"behavior": {
"name": "[%key:component::power::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::power::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::power::common::condition_threshold_name%]"
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["prowl"],
"requirements": ["prowlpy==1.1.1"]
"requirements": ["prowlpy==1.1.5"]
}
@@ -97,6 +97,19 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
**auth_kwargs,
)
except AuthenticationError as err:
raise ProxmoxAuthenticationError from err
except SSLError as err:
raise ProxmoxSSLError from err
except ConnectTimeout as err:
raise ProxmoxConnectTimeout from err
except ResourceException as err:
_LOGGER.debug("Error during Proxmox client initialisation", exc_info=True)
raise ProxmoxInitFailed from err
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err
try:
nodes = client.nodes.get()
except AuthenticationError as err:
raise ProxmoxAuthenticationError from err
@@ -105,6 +118,7 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
except ConnectTimeout as err:
raise ProxmoxConnectTimeout from err
except ResourceException as err:
_LOGGER.debug("Error fetching nodes", exc_info=True)
raise ProxmoxNoNodesFound from err
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err
@@ -115,7 +129,10 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
vms = client.nodes(node["node"]).qemu.get()
containers = client.nodes(node["node"]).lxc.get()
except ResourceException as err:
raise ProxmoxNoNodesFound from err
_LOGGER.debug(
"Error fetching VMs/LXC for node %s", node["node"], exc_info=True
)
raise ProxmoxNoVMLXCFound from err
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err
@@ -298,9 +315,15 @@ class ProxmoxveConfigFlow(ConfigFlow, domain=DOMAIN):
except ProxmoxSSLError as exc:
errors["base"] = "ssl_error"
err = exc
except ProxmoxInitFailed as exc:
errors["base"] = "api_error_no_details"
err = exc
except ProxmoxNoNodesFound as exc:
errors["base"] = "no_nodes_found"
err = exc
except ProxmoxNoVMLXCFound as exc:
errors["base"] = "no_vmlxc_found"
err = exc
except ProxmoxConnectionError as exc:
errors["base"] = "cannot_connect"
err = exc
@@ -370,6 +393,14 @@ class ProxmoxNoNodesFound(ProxmoxError):
"""Error to indicate no nodes found."""
class ProxmoxNoVMLXCFound(ProxmoxError):
"""Error to indicate no LXC or VM found."""
class ProxmoxInitFailed(ProxmoxError):
"""Error to indicate API initialisation failure."""
class ProxmoxConnectTimeout(ProxmoxError):
"""Error to indicate a connection timeout."""
@@ -6,10 +6,12 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"api_error_no_details": "An error occurred while communicating with the Proxmox VE instance.",
"cannot_connect": "Cannot connect to Proxmox VE server",
"connect_timeout": "[%key:common::config_flow::error::timeout_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"no_nodes_found": "No active nodes found",
"no_nodes_found": "No active nodes were found on the Proxmox VE server.",
"no_vmlxc_found": "No LXC or VM were found on the Proxmox VE server.",
"ssl_error": "SSL check failed. Check the SSL settings"
},
"step": {
@@ -324,6 +326,9 @@
"no_permission_vm_lxc_power": {
"message": "The configured Proxmox VE user does not have permission to manage the power state of VMs and containers. Please grant the user the 'VM.PowerMgmt' permission and try again."
},
"no_vmlxc_found": {
"message": "No LXC or VM were found on the Proxmox VE server."
},
"permissions_error": {
"message": "Failed to retrieve Proxmox VE permissions. Please check your credentials and try again."
},
+2 -2
View File
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -12,13 +12,13 @@
},
"error": {
"auth_missing": "[%key:component::samsungtv::config::abort::auth_missing%]",
"invalid_host": "Host is invalid, please try again.",
"invalid_pin": "PIN is invalid, please try again."
"invalid_host": "[%key:common::config_flow::error::invalid_host%]",
"invalid_pin": "The PIN is invalid. Please try again."
},
"flow_title": "{device}",
"step": {
"confirm": {
"description": "Do you want to set up {device}? If you never connected Home Assistant before you should see a popup on your TV asking for authorization."
"description": "Do you want to set up {device}? If you have never connected Home Assistant before, you should see a popup on your TV asking for authorization."
},
"encrypted_pairing": {
"data": {
@@ -62,7 +62,7 @@
"host": "The hostname or IP address of your TV.",
"name": "The name of your TV. This will be used to identify the device in Home Assistant."
},
"description": "Enter your Samsung TV information. If you never connected Home Assistant before you should see a popup on your TV asking for authorization."
"description": "Enter your Samsung TV information. If you have never connected Home Assistant before, you should see a popup on your TV asking for authorization."
}
}
},
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["satel_integra"],
"quality_scale": "bronze",
"requirements": ["satel-integra==1.2.2"]
"requirements": ["satel-integra==1.3.0"]
}
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
@@ -62,6 +62,7 @@ async def async_setup_platform(
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=None,
name="schluter",
update_method=async_update_data,
update_interval=SCAN_INTERVAL,
@@ -10,6 +10,11 @@ is_option_selected:
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
option:
context:
filter_target: target
@@ -6,6 +6,9 @@
"behavior": {
"name": "Condition passes if"
},
"for": {
"name": "For at least"
},
"option": {
"description": "The options to check for.",
"name": "Option"
@@ -17,7 +17,7 @@
"iot_class": "local_push",
"loggers": ["aioshelly"],
"quality_scale": "platinum",
"requirements": ["aioshelly==13.24.2"],
"requirements": ["aioshelly==13.25.0"],
"zeroconf": [
{
"name": "shelly*",
+2 -2
View File
@@ -7,8 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from . import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF, support_duration=True),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON, support_duration=True),
"is_off": make_entity_state_condition(DOMAIN, STATE_OFF),
"is_on": make_entity_state_condition(DOMAIN, STATE_ON),
}
+2 -6
View File
@@ -11,12 +11,8 @@ from .const import DOMAIN
SWITCH_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_BOOLEAN_DOMAIN: DomainSpec()}
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(
SWITCH_DOMAIN_SPECS, STATE_OFF, support_duration=True
),
"is_on": make_entity_state_condition(
SWITCH_DOMAIN_SPECS, STATE_ON, support_duration=True
),
"is_off": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_OFF),
"is_on": make_entity_state_condition(SWITCH_DOMAIN_SPECS, STATE_ON),
}
@@ -2,9 +2,10 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, cast
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any, cast
from synology_dsm.api.core.external_usb import (
SynoCoreExternalUSB,
@@ -32,6 +33,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from . import SynoApi
from .const import CONF_VOLUMES, ENTITY_UNIT_LOAD
@@ -49,6 +51,8 @@ class SynologyDSMSensorEntityDescription(
):
"""Describes Synology DSM sensor entity."""
value_fn: Callable[[SynoDSMInformation, str], Any] = getattr
UTILISATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
SynologyDSMSensorEntityDescription(
@@ -326,6 +330,9 @@ INFORMATION_SENSORS: tuple[SynologyDSMSensorEntityDescription, ...] = (
SynologyDSMSensorEntityDescription(
api_key=SynoDSMInformation.API_KEY,
key="uptime",
value_fn=lambda api_information, _: (
utcnow() - timedelta(seconds=api_information.uptime)
),
device_class=SensorDeviceClass.UPTIME,
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -546,8 +553,12 @@ class SynoDSMInfoSensor(SynoDSMSensor):
@property
def native_value(self) -> StateType | datetime:
"""Return the state."""
attr = getattr(self._api.information, self.entity_description.key)
if attr is None:
if self._api.information is None:
return None
return attr # type: ignore[no-any-return]
return cast(
StateType | datetime,
self.entity_description.value_fn(
self._api.information, self.entity_description.key
),
)
+1 -1
View File
@@ -562,7 +562,7 @@ class TelegramNotificationService:
authentication=entry.get(ATTR_AUTHENTICATION),
verify_ssl=entry[ATTR_VERIFY_SSL],
)
_LOGGER.debug("downloaded: %s", entry[ATTR_URL])
_LOGGER.debug("downloaded: %s", entry.get(ATTR_URL) or entry.get(ATTR_FILE))
caption: str | None = entry.get(ATTR_CAPTION)
if entry[ATTR_MEDIA_TYPE] == InputMediaType.AUDIO:
@@ -25,6 +25,11 @@ is_value:
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
threshold:
required: true
selector:
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold type",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least",
@@ -13,6 +14,9 @@
"behavior": {
"name": "[%key:component::temperature::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::temperature::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::temperature::common::condition_threshold_name%]"
}
+15 -15
View File
@@ -11,7 +11,6 @@ import voluptuous as vol
from homeassistant.components.vacuum import (
ATTR_FAN_SPEED,
DOMAIN as VACUUM_DOMAIN,
SERVICE_CLEAN_AREA,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
@@ -62,13 +61,14 @@ from .trigger_entity import TriggerEntity
_LOGGER = logging.getLogger(__name__)
CONF_VACUUMS = "vacuums"
CONF_BATTERY_LEVEL = "battery_level"
CONF_BATTERY_LEVEL_TEMPLATE = "battery_level_template"
CONF_FAN_SPEED_LIST = "fan_speeds"
CONF_CLEAN_SEGMENTS = "clean_segments"
CONF_FAN_SPEED = "fan_speed"
CONF_FAN_SPEED_LIST = "fan_speeds"
CONF_FAN_SPEED_TEMPLATE = "fan_speed_template"
CONF_SEGMENTS_TEMPLATE = "segments_template"
CONF_SEGMENTS = "segments"
CONF_VACUUMS = "vacuums"
DEFAULT_NAME = "Template Vacuum"
@@ -81,7 +81,7 @@ LEGACY_FIELDS = {
}
SCRIPT_FIELDS = (
SERVICE_CLEAN_AREA,
CONF_CLEAN_SEGMENTS,
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
@@ -100,9 +100,9 @@ VACUUM_COMMON_SCHEMA = vol.Schema(
vol.Optional(CONF_FAN_SPEED): cv.template,
vol.Optional(CONF_STATE): cv.template,
vol.Inclusive(
CONF_SEGMENTS_TEMPLATE,
CONF_SEGMENTS,
CLEAN_AREA_GROUP,
f"Options `{CONF_SEGMENTS_TEMPLATE}` and `{SERVICE_CLEAN_AREA}` must both exist",
f"Options `{CONF_SEGMENTS}` and `{CONF_CLEAN_SEGMENTS}` must both exist",
): cv.template,
vol.Optional(SERVICE_CLEAN_SPOT): cv.SCRIPT_SCHEMA,
vol.Optional(SERVICE_LOCATE): cv.SCRIPT_SCHEMA,
@@ -112,9 +112,9 @@ VACUUM_COMMON_SCHEMA = vol.Schema(
vol.Required(SERVICE_START): cv.SCRIPT_SCHEMA,
vol.Optional(SERVICE_STOP): cv.SCRIPT_SCHEMA,
vol.Inclusive(
SERVICE_CLEAN_AREA,
CONF_CLEAN_SEGMENTS,
CLEAN_AREA_GROUP,
f"Options `{CONF_SEGMENTS_TEMPLATE}` and `{SERVICE_CLEAN_AREA}` must both exist",
f"Options `{CONF_SEGMENTS}` and `{CONF_CLEAN_SEGMENTS}` must both exist",
): cv.SCRIPT_SCHEMA,
}
)
@@ -126,8 +126,8 @@ VACUUM_YAML_SCHEMA = vol.All(
VACUUM_DOMAIN, DEFAULT_NAME
).schema
),
cv.key_dependency(CONF_SEGMENTS_TEMPLATE, CONF_UNIQUE_ID),
cv.key_dependency(SERVICE_CLEAN_AREA, CONF_UNIQUE_ID),
cv.key_dependency(CONF_SEGMENTS, CONF_UNIQUE_ID),
cv.key_dependency(CONF_CLEAN_SEGMENTS, CONF_UNIQUE_ID),
)
VACUUM_LEGACY_YAML_SCHEMA = vol.All(
@@ -320,9 +320,9 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
)
self.setup_template(
CONF_SEGMENTS_TEMPLATE,
CONF_SEGMENTS,
"_segments",
validate_segments(self, CONF_SEGMENTS_TEMPLATE),
validate_segments(self, CONF_SEGMENTS),
self._update_segments,
)
@@ -341,7 +341,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
(SERVICE_CLEAN_SPOT, VacuumEntityFeature.CLEAN_SPOT),
(SERVICE_LOCATE, VacuumEntityFeature.LOCATE),
(SERVICE_SET_FAN_SPEED, VacuumEntityFeature.FAN_SPEED),
(SERVICE_CLEAN_AREA, VacuumEntityFeature.CLEAN_AREA),
(CONF_CLEAN_SEGMENTS, VacuumEntityFeature.CLEAN_AREA),
):
if (action_config := config.get(action_id)) is not None:
self.add_script(action_id, action_config, name, DOMAIN)
@@ -369,7 +369,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
if self._attr_assumed_state:
self._attr_activity = VacuumActivity.CLEANING
self.async_write_ha_state()
if script := self._action_scripts.get(SERVICE_CLEAN_AREA):
if script := self._action_scripts.get(CONF_CLEAN_SEGMENTS):
await self.async_run_script(
script,
run_variables={"segment_ids": segment_ids},
@@ -45,6 +45,11 @@ class TextIsEqualToCondition(EntityConditionBase):
assert config.options
self._value: str = config.options[CONF_VALUE]
@property
def _needs_duration_tracking(self) -> bool:
"""Return if this condition needs duration tracking."""
return False
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the state matches the expected value."""
return entity_state.state == self._value
@@ -10,6 +10,11 @@ is_equal_to:
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
value:
required: true
selector:
+5 -1
View File
@@ -1,6 +1,7 @@
{
"common": {
"condition_behavior_name": "Condition passes if"
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least"
},
"conditions": {
"is_equal_to": {
@@ -9,6 +10,9 @@
"behavior": {
"name": "[%key:component::text::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::text::common::condition_for_name%]"
},
"value": {
"description": "The value to compare the text to.",
"name": "Value"
+3 -7
View File
@@ -6,13 +6,9 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from . import DOMAIN, STATUS_ACTIVE, STATUS_IDLE, STATUS_PAUSED
CONDITIONS: dict[str, type[Condition]] = {
"is_active": make_entity_state_condition(
DOMAIN, STATUS_ACTIVE, support_duration=True
),
"is_paused": make_entity_state_condition(
DOMAIN, STATUS_PAUSED, support_duration=True
),
"is_idle": make_entity_state_condition(DOMAIN, STATUS_IDLE, support_duration=True),
"is_active": make_entity_state_condition(DOMAIN, STATUS_ACTIVE),
"is_paused": make_entity_state_condition(DOMAIN, STATUS_PAUSED),
"is_idle": make_entity_state_condition(DOMAIN, STATUS_IDLE),
}
+1 -1
View File
@@ -10,7 +10,7 @@ from homeassistant.helpers.condition import (
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"all_completed": make_entity_state_condition(DOMAIN, "0", support_duration=True),
"all_completed": make_entity_state_condition(DOMAIN, "0"),
"incomplete": make_entity_numerical_condition(DOMAIN),
}
@@ -9,7 +9,7 @@
selector:
automation_behavior:
mode: condition
for:
for: &condition_for
required: true
default: 00:00:00
selector:
@@ -30,6 +30,7 @@ incomplete:
target: *condition_todo_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -23,6 +23,9 @@
"behavior": {
"name": "[%key:component::todo::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::todo::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::todo::common::condition_threshold_name%]"
}
+2 -6
View File
@@ -7,12 +7,8 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN
CONDITIONS: dict[str, type[Condition]] = {
"is_available": make_entity_state_condition(
DOMAIN, STATE_ON, support_duration=True
),
"is_not_available": make_entity_state_condition(
DOMAIN, STATE_OFF, support_duration=True
),
"is_available": make_entity_state_condition(DOMAIN, STATE_ON),
"is_not_available": make_entity_state_condition(DOMAIN, STATE_OFF),
}
+1 -1
View File
@@ -24,7 +24,7 @@ async def async_setup_entry(
class UptimeSensor(SensorEntity):
"""Representation of an uptime sensor."""
_attr_device_class = SensorDeviceClass.TIMESTAMP
_attr_device_class = SensorDeviceClass.UPTIME
_attr_has_entity_name = True
_attr_name = None
_attr_should_poll = False
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyuptimerobot"],
"quality_scale": "gold",
"requirements": ["pyuptimerobot==24.0.1"]
"requirements": ["pyuptimerobot==25.0.0"]
}
@@ -61,9 +61,12 @@ class UptimeRobotSensor(UptimeRobotEntity, SensorEntity):
"""Representation of a UptimeRobot sensor."""
@property
def native_value(self) -> str:
def native_value(self) -> str | None:
"""Return the status of the monitor."""
if not self._monitor.status:
return None
status = self._monitor.status.lower()
# The API returns "paused"
# but the entity state will be "pause" to avoid a breaking change
return {"paused": "pause"}.get(status, status) # type: ignore[no-any-return]
return {"paused": "pause"}.get(status, status)
+9 -26
View File
@@ -4,11 +4,7 @@ from __future__ import annotations
from typing import Any
from pyuptimerobot import (
UptimeRobotAuthenticationException,
UptimeRobotException,
UptimeRobotMonitor,
)
from pyuptimerobot import UptimeRobotMonitor
from homeassistant.components.switch import (
SwitchDeviceClass,
@@ -16,13 +12,12 @@ from homeassistant.components.switch import (
SwitchEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, STATUS_DOWN, STATUS_UP
from .const import STATUS_UP
from .coordinator import UptimeRobotConfigEntry
from .entity import UptimeRobotEntity
from .utils import new_device_listener
from .utils import new_device_listener, uptimerobot_api_call
# Limit the number of parallel updates to 1
PARALLEL_UPDATES = 1
@@ -65,26 +60,14 @@ class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity):
"""Return True if the entity is on."""
return bool(self._monitor.status == STATUS_UP)
async def _async_edit_monitor(self, **kwargs: Any) -> None:
"""Edit monitor status."""
try:
await self.api.async_edit_monitor(**kwargs)
except UptimeRobotAuthenticationException:
self.coordinator.config_entry.async_start_reauth(self.hass)
return
except UptimeRobotException as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
await self.coordinator.async_request_refresh()
@uptimerobot_api_call
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off switch."""
await self._async_edit_monitor(monitor_id=self._monitor.id, status=STATUS_DOWN)
await self.api.async_pause_monitor(monitor_id=self._monitor.id)
await self.coordinator.async_request_refresh()
@uptimerobot_api_call
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on switch."""
await self._async_edit_monitor(monitor_id=self._monitor.id, status=STATUS_UP)
await self.api.async_start_monitor(monitor_id=self._monitor.id)
await self.coordinator.async_request_refresh()
+35 -2
View File
@@ -2,11 +2,44 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from typing import Any, Concatenate
from pyuptimerobot import UptimeRobotMonitor
from pyuptimerobot import (
UptimeRobotAuthenticationException,
UptimeRobotException,
UptimeRobotMonitor,
)
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .coordinator import UptimeRobotDataUpdateCoordinator
from .entity import UptimeRobotEntity
def uptimerobot_api_call[_T: UptimeRobotEntity, **_P](
func: Callable[Concatenate[_T, _P], Awaitable[None]],
) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]:
"""Catch UptimeRobot API call exceptions."""
@wraps(func)
async def cmd_wrapper(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None:
"""Wrap all command methods."""
try:
await func(self, *args, **kwargs)
except UptimeRobotAuthenticationException:
self.coordinator.config_entry.async_start_reauth(self.hass)
return
except UptimeRobotException as exception:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="api_exception",
translation_placeholders={"error": "Generic UptimeRobot exception"},
) from exception
return cmd_wrapper
def new_device_listener(
+5 -13
View File
@@ -6,21 +6,13 @@ from homeassistant.helpers.condition import Condition, make_entity_state_conditi
from .const import DOMAIN, VacuumActivity
CONDITIONS: dict[str, type[Condition]] = {
"is_cleaning": make_entity_state_condition(
DOMAIN, VacuumActivity.CLEANING, support_duration=True
),
"is_docked": make_entity_state_condition(
DOMAIN, VacuumActivity.DOCKED, support_duration=True
),
"is_cleaning": make_entity_state_condition(DOMAIN, VacuumActivity.CLEANING),
"is_docked": make_entity_state_condition(DOMAIN, VacuumActivity.DOCKED),
"is_encountering_an_error": make_entity_state_condition(
DOMAIN, VacuumActivity.ERROR, support_duration=True
),
"is_paused": make_entity_state_condition(
DOMAIN, VacuumActivity.PAUSED, support_duration=True
),
"is_returning": make_entity_state_condition(
DOMAIN, VacuumActivity.RETURNING, support_duration=True
DOMAIN, VacuumActivity.ERROR
),
"is_paused": make_entity_state_condition(DOMAIN, VacuumActivity.PAUSED),
"is_returning": make_entity_state_condition(DOMAIN, VacuumActivity.RETURNING),
}
@@ -9,6 +9,11 @@
selector:
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
is_open: *condition_common
is_closed: *condition_common

Some files were not shown because too many files have changed in this diff Show More