Compare commits

...

98 Commits

Author SHA1 Message Date
Franck Nijhof b193d951d7 Bump version to 2026.5.0 2026-05-06 15:01:09 +00:00
Franck Nijhof 4cd0d9dcec Bump version to 2026.5.0b4 2026-05-06 13:27:18 +00:00
Daniel Hjelseth Høyer 32f65b2e11 Bump pyTibber to 0.37.4 (#169907) 2026-05-06 13:27:09 +00:00
Erik Montnemery 8c79d1e44b Remove _get_tracked_value method from EntityConditionBase (#169906) 2026-05-06 13:27:07 +00:00
Erik Montnemery 8d53f7a520 Exclude incompatible humidifier entities from humidifier automations (#169905) 2026-05-06 13:27:05 +00:00
Erik Montnemery cc83ee88fb Exclude incompatible water_heater entities from water_heater automations (#169904) 2026-05-06 13:27:03 +00:00
Erik Montnemery 0c5b02eff3 Exclude incompatible climate entities from climate automations (#169903) 2026-05-06 13:27:02 +00:00
Erik Montnemery 9da9f8fd50 Unload scripts and conditions created by template entities (#169366) 2026-05-06 13:27:00 +00:00
Franck Nijhof d70ffcd3e9 Bump version to 2026.5.0b3 2026-05-06 11:16:10 +00:00
Erik Montnemery 3e26d0dfe3 Exclude incompatible entities from temperature automations (#169901) 2026-05-06 11:15:56 +00:00
Erik Montnemery eab9747b32 Exclude incompatible entities from humidity automations (#169898) 2026-05-06 11:15:54 +00:00
Erik Montnemery 9e955d8294 Add media_player volume condition (#169897) 2026-05-06 11:15:52 +00:00
Bram Kragten f08cd01ff8 Update frontend to 20260429.3 (#169893) 2026-05-06 11:10:49 +00:00
Erik Montnemery eabaf3b0fe Add media_player muted conditions (#169892) 2026-05-06 11:10:47 +00:00
Tom Matheussen 65ca790d15 Bump satel_integra to 1.3.1 (#169889) 2026-05-06 11:10:45 +00:00
Joost Lekkerkerker d177944f7a Fix Zinvolt select options (#169886) 2026-05-06 11:10:43 +00:00
Erik Montnemery 7f186f4430 Add media_player volume triggers (#169885) 2026-05-06 11:10:41 +00:00
Erik Montnemery 4f4f4642a7 Add method _should_include to EntityConditionBase (#169884)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-06 11:10:39 +00:00
Erik Montnemery 12e443cd31 Improve entity trigger tests (#169881) 2026-05-06 11:10:37 +00:00
Erik Montnemery 22a7daabe7 Add method _should_include to EntityTriggerBase (#169837) 2026-05-06 11:10:35 +00:00
Erik Montnemery c139e99abd Improve condition test helper docstrings (#169871) 2026-05-06 11:09:06 +00:00
Erik Montnemery 2bfdb96a3f Improve trigger test helper docstrings (#169869) 2026-05-06 11:09:04 +00:00
puddly 4b24ca924b Bump serialx to 1.7.0 (#169867) 2026-05-06 11:09:02 +00:00
Michael Hansen 1d3d714e4f Bump intents to 2026.5.5 (#169855) 2026-05-06 11:09:00 +00:00
Erik Montnemery ffae6eda8a Validate yaml matches implementation in automation options_supported tests (#169798) 2026-05-06 11:05:41 +00:00
Erik Montnemery 4dd996b728 Add trigger media_player.unmuted (#169797) 2026-05-06 11:05:40 +00:00
Erik Montnemery afad1e8dac Improve mobile_app device tracker tests (#169724) 2026-05-06 11:05:38 +00:00
Manu 8e41933251 Record notification from legacy notify action in Mobile App (#169749) 2026-05-06 11:00:21 +00:00
Erik Montnemery c581eaad53 Add trigger timer.time_remaining (#169763) 2026-05-06 10:58:59 +00:00
Ludovic BOUÉ 3050e79d06 Expose SET_SPEED for all fans via PercentSetting in Matter (#169696)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-05-06 08:55:15 +00:00
Andres Ruiz 0e8ecd1065 Catch additional errors as potentially retryable errors during energy data updates (#169646) 2026-05-06 08:55:13 +00:00
Paulus Schoutsen 94732139f4 Bump version to 2026.5.0b2 2026-05-05 10:29:37 -04:00
Denis Shulyaka c5e08b2409 Return the requested format for OpenAI TTS (#169839)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-05 10:29:30 -04:00
Joost Lekkerkerker c12e1b5f4a Add Zunzunbee Zigbee brand (#169838) 2026-05-05 10:29:29 -04:00
Joost Lekkerkerker 6cfedb55e6 Add Sensereo matter brand (#169836) 2026-05-05 10:29:27 -04:00
Åke Strandberg af4cb9530b Add missing code for miele washing machine (#169795) 2026-05-05 10:29:26 -04:00
Matthias Alphart 58e97e7d5f Update xknxproject to 3.9.0 (#169775) 2026-05-05 10:29:25 -04:00
Daniel Hjelseth Høyer 2945b51617 Bump pyTibber to 0.37.3 (#169762) 2026-05-05 10:29:24 -04:00
Keilin Bickar 9d0e2df627 bump sense-energy to 0.14.1 (#169761) 2026-05-05 10:29:23 -04:00
Steve Syrell 643ae080db Bump Insteon-panel to 0.6.2 (#169757) 2026-05-05 10:29:22 -04:00
G Johansson a7eaa51179 Fix config flow validation in Nord Pool (#169751) 2026-05-05 10:29:21 -04:00
Petro31 e15852ff38 Fix uptime template sensor (#169743) 2026-05-05 10:29:20 -04:00
Diogo Gomes f6dec34136 Bump pytrydan to 1.0.0 (#169742) 2026-05-05 10:29:19 -04:00
Raj Laud 53905fbc49 Bump victron-ble-ha-parser to 0.7.0 (#169736)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-05 10:29:17 -04:00
Thomas D 8218ff0fe8 Add missing initialization charging power status option to Volvo (#169727) 2026-05-05 10:29:16 -04:00
kernelpanic85 663f7e3e6b Add Celsius and Fahrenheit to Smartthings UNITS mapping (#169686) 2026-05-05 10:29:15 -04:00
Nathan Spencer 4dfa2b8b88 Limit power status binary sensor to non-LR5 devices (#169659) 2026-05-05 10:29:14 -04:00
Nathan Spencer f828b165b1 Bump pylitterbot to 2025.4.0 (#169652) 2026-05-05 10:29:13 -04:00
shbatm c56c506648 Add precipitation device class to WeatherFlow Cloud accumulation sensors (#169638)
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 10:29:12 -04:00
Artur Pragacz 8e5bf2a35f Fix async_unload teardown race in scripts (#169562) 2026-05-05 10:29:10 -04:00
Erik Montnemery 4d575e69a4 Improve template reload (#169480) 2026-05-05 10:29:09 -04:00
Christian Lackas 4f78bbccc0 Use all_devices in ViCare diagnostics for completeness (#169429) 2026-05-05 10:29:08 -04:00
Erik Montnemery 2d66ebe54a Add trigger media_player.muted (#156736) 2026-05-05 10:29:07 -04:00
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
250 changed files with 6835 additions and 1426 deletions
Generated
+2 -2
View File
@@ -851,8 +851,8 @@ CLAUDE.md @home-assistant/core
/tests/components/input_select/ @home-assistant/core
/homeassistant/components/input_text/ @home-assistant/core
/tests/components/input_text/ @home-assistant/core
/homeassistant/components/insteon/ @teharris1
/tests/components/insteon/ @teharris1
/homeassistant/components/insteon/ @teharris1 @ssyrell
/tests/components/insteon/ @teharris1 @ssyrell
/homeassistant/components/integration/ @dgomes
/tests/components/integration/ @dgomes
/homeassistant/components/intelliclima/ @dvdinth
+5
View File
@@ -0,0 +1,5 @@
{
"domain": "sensereo",
"name": "Sensereo",
"iot_standards": ["matter"]
}
+5
View File
@@ -0,0 +1,5 @@
{
"domain": "zunzunbee",
"name": "Zunzunbee",
"iot_standards": ["zigbee"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["serialx==1.4.1"]
"requirements": ["serialx==1.7.0"]
}
@@ -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
),
}
@@ -901,12 +901,13 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
async def async_will_remove_from_hass(self) -> None:
"""Remove listeners when removing automation from Home Assistant."""
await super().async_will_remove_from_hass()
await self._async_disable()
if self.registry_entry and self.registry_entry.entity_id != self.entity_id:
# Entity ID change, do not unload the script or conditions as they will
# be reused.
await self._async_disable()
return
self.action_script.async_unload()
await self._async_disable(stop_actions=False)
await self.action_script.async_unload()
if self._condition is not None:
self._condition.async_unload()
@@ -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),
}
+24 -6
View File
@@ -13,8 +13,8 @@ from homeassistant.helpers.condition import (
Condition,
ConditionConfig,
EntityConditionBase,
EntityNumericalConditionBase,
EntityNumericalConditionWithUnitBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.util.unit_conversion import TemperatureConverter
@@ -59,15 +59,36 @@ class ClimateTargetTemperatureCondition(EntityNumericalConditionWithUnitBase):
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target temperature."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_TEMPERATURE) is not None
)
def _get_entity_unit(self, entity_state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
return self._hass.config.units.temperature_unit
class ClimateTargetHumidityCondition(EntityNumericalConditionBase):
"""Condition for climate target humidity."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}
_valid_unit = "%"
def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target humidity."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_HUMIDITY) is not None
)
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,
{
@@ -88,10 +109,7 @@ CONDITIONS: dict[str, type[Condition]] = {
"is_heating": make_entity_state_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.HEATING
),
"target_humidity": make_entity_numerical_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity": ClimateTargetHumidityCondition,
"target_temperature": ClimateTargetTemperatureCondition,
}
@@ -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%]"
}
+38 -10
View File
@@ -8,14 +8,15 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateChangedTriggerWithUnitBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
EntityNumericalStateTriggerBase,
EntityNumericalStateTriggerWithUnitBase,
EntityTargetStateTriggerBase,
Trigger,
TriggerConfig,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
make_entity_target_state_trigger,
make_entity_transition_trigger,
)
@@ -55,6 +56,13 @@ class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitB
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_TEMPERATURE)}
_unit_converter = TemperatureConverter
def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target temperature."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_TEMPERATURE) is not None
)
def _get_entity_unit(self, state: State) -> str | None:
"""Get the temperature unit of a climate entity from its state."""
# Climate entities convert temperatures to the system unit via show_temp
@@ -75,6 +83,32 @@ class ClimateTargetTemperatureCrossedThresholdTrigger(
"""Trigger for climate target temperature value crossing a threshold."""
class _ClimateTargetHumidityTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for climate target humidity triggers."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}
_valid_unit = "%"
def _should_include(self, state: State) -> bool:
"""Skip climate entities that do not expose a target humidity."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_HUMIDITY) is not None
)
class ClimateTargetHumidityChangedTrigger(
_ClimateTargetHumidityTriggerMixin, EntityNumericalStateChangedTriggerBase
):
"""Trigger for climate target humidity value changes."""
class ClimateTargetHumidityCrossedThresholdTrigger(
_ClimateTargetHumidityTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
):
"""Trigger for climate target humidity value crossing a threshold."""
TRIGGERS: dict[str, type[Trigger]] = {
"hvac_mode_changed": HVACModeChangedTrigger,
"started_cooling": make_entity_target_state_trigger(
@@ -83,14 +117,8 @@ TRIGGERS: dict[str, type[Trigger]] = {
"started_drying": make_entity_target_state_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
),
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit="%",
),
"target_humidity_changed": ClimateTargetHumidityChangedTrigger,
"target_humidity_crossed_threshold": ClimateTargetHumidityCrossedThresholdTrigger,
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
@@ -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)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.3.24"]
"requirements": ["hassil==3.5.0", "home-assistant-intents==2026.5.5"]
}
@@ -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
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.14.0"]
"requirements": ["sense-energy==0.14.1"]
}
+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.3"]
}
+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],
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
import voluptuous as vol
from homeassistant.const import ATTR_MODE, CONF_OPTIONS, PERCENTAGE, STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import DomainSpec
@@ -13,8 +13,8 @@ from homeassistant.helpers.condition import (
ENTITY_STATE_CONDITION_SCHEMA_ANY_ALL,
Condition,
ConditionConfig,
EntityNumericalConditionBase,
EntityStateConditionBase,
make_entity_numerical_condition,
make_entity_state_condition,
)
from homeassistant.helpers.entity import get_supported_features
@@ -46,6 +46,20 @@ def _supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> boo
return False
class IsTargetHumidityCondition(EntityNumericalConditionBase):
"""Condition for humidifier target humidity."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)}
_valid_unit = PERCENTAGE
def _should_include(self, state: State) -> bool:
"""Skip humidifier entities that do not expose a target humidity."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_HUMIDITY) is not None
)
class IsModeCondition(EntityStateConditionBase):
"""Condition for humidifier mode."""
@@ -70,8 +84,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
),
@@ -79,10 +93,7 @@ CONDITIONS: dict[str, type[Condition]] = {
{DOMAIN: DomainSpec(value_source=ATTR_ACTION)}, HumidifierAction.HUMIDIFYING
),
"is_mode": IsModeCondition,
"is_target_humidity": make_entity_numerical_condition(
{DOMAIN: DomainSpec(value_source=ATTR_HUMIDITY)},
valid_unit=PERCENTAGE,
),
"is_target_humidity": IsTargetHumidityCondition,
}
@@ -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%]"
}
+26 -3
View File
@@ -16,9 +16,9 @@ from homeassistant.components.weather import (
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import Condition, make_entity_numerical_condition
from homeassistant.helpers.condition import Condition, EntityNumericalConditionBase
HUMIDITY_DOMAIN_SPECS = {
CLIMATE_DOMAIN: DomainSpec(
@@ -33,8 +33,31 @@ HUMIDITY_DOMAIN_SPECS = {
),
}
class HumidityCondition(EntityNumericalConditionBase):
"""Condition for humidity value across multiple domains."""
_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = PERCENTAGE
def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.
Mirrors the humidity trigger: for climate / humidifier / weather
(attribute-based), the entity is filtered when the source attribute
is absent; sensor entities (state-value-based) fall through to the
base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None
CONDITIONS: dict[str, type[Condition]] = {
"is_value": make_entity_numerical_condition(HUMIDITY_DOMAIN_SPECS, PERCENTAGE),
"is_value": HumidityCondition,
}
@@ -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%]"
}
+43 -9
View File
@@ -15,12 +15,13 @@ from homeassistant.components.weather import (
ATTR_WEATHER_HUMIDITY,
DOMAIN as WEATHER_DOMAIN,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
Trigger,
make_entity_numerical_state_changed_trigger,
make_entity_numerical_state_crossed_threshold_trigger,
)
HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
@@ -38,13 +39,46 @@ HUMIDITY_DOMAIN_SPECS: dict[str, DomainSpec] = {
),
}
class _HumidityTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for humidity triggers providing entity filtering."""
_domain_specs = HUMIDITY_DOMAIN_SPECS
_valid_unit = "%"
def _should_include(self, state: State) -> bool:
"""Skip attribute-source entities that lack the humidity attribute.
For domains whose tracked value comes from an attribute
(climate / humidifier / weather), require the attribute to be
present; otherwise the all/count check would treat an entity that
cannot report a humidity as a non-match and block behavior=last.
Sensor entities source their value from `state.state`, so they
fall through to the base impl.
"""
if not super()._should_include(state):
return False
domain_spec = self._domain_specs[state.domain]
if domain_spec.value_source is None:
return True
return state.attributes.get(domain_spec.value_source) is not None
class HumidityChangedTrigger(
_HumidityTriggerMixin, EntityNumericalStateChangedTriggerBase
):
"""Trigger for humidity value changes across multiple domains."""
class HumidityCrossedThresholdTrigger(
_HumidityTriggerMixin, EntityNumericalStateCrossedThresholdTriggerBase
):
"""Trigger for humidity value crossing a threshold across multiple domains."""
TRIGGERS: dict[str, type[Trigger]] = {
"changed": make_entity_numerical_state_changed_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
HUMIDITY_DOMAIN_SPECS, valid_unit="%"
),
"changed": HumidityChangedTrigger,
"crossed_threshold": HumidityCrossedThresholdTrigger,
}
@@ -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%]"
}
@@ -2,7 +2,7 @@
"domain": "insteon",
"name": "Insteon",
"after_dependencies": ["panel_custom"],
"codeowners": ["@teharris1"],
"codeowners": ["@teharris1", "@ssyrell"],
"config_flow": true,
"dependencies": ["http", "usb", "websocket_api"],
"dhcp": [
@@ -19,7 +19,7 @@
"loggers": ["pyinsteon", "pypubsub"],
"requirements": [
"pyinsteon==1.6.4",
"insteon-frontend-home-assistant==0.6.1"
"insteon-frontend-home-assistant==0.6.2"
],
"single_config_entry": true,
"usb": [
@@ -79,10 +79,9 @@ async def async_reload(hass: HomeAssistant, service_call: ServiceCall) -> None:
existing_intents = hass.data[DOMAIN]
for intent_type, conf in existing_intents.items():
if isinstance(conf.get(CONF_ACTION), script.Script):
await conf[CONF_ACTION].async_stop()
conf[CONF_ACTION].async_unload()
intent.async_remove(hass, intent_type)
if isinstance(conf.get(CONF_ACTION), script.Script):
await conf[CONF_ACTION].async_unload()
if not new_config or DOMAIN not in new_config:
hass.data[DOMAIN] = {}
+2 -2
View File
@@ -12,8 +12,8 @@
"quality_scale": "platinum",
"requirements": [
"xknx==3.15.0",
"xknxproject==3.8.2",
"knx-frontend==2026.4.25.155016"
"xknxproject==3.9.0",
"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%]"
}
@@ -6,7 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic
from pylitterbot import LitterRobot, LitterRobot4, Robot
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4, Robot
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -32,8 +32,11 @@ class RobotBinarySensorEntityDescription(
is_on_fn: Callable[[_WhiskerEntityT], bool]
BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, ...]] = {
LitterRobot: ( # type: ignore[type-abstract] # only used for isinstance check
BINARY_SENSOR_MAP: dict[
type[Robot] | tuple[type[Robot], ...],
tuple[RobotBinarySensorEntityDescription, ...],
] = {
LitterRobot: (
RobotBinarySensorEntityDescription[LitterRobot](
key="sleeping",
translation_key="sleeping",
@@ -58,14 +61,14 @@ BINARY_SENSOR_MAP: dict[type[Robot], tuple[RobotBinarySensorEntityDescription, .
is_on_fn=lambda robot: not robot.is_hopper_removed,
),
),
Robot: ( # type: ignore[type-abstract] # only used for isinstance check
RobotBinarySensorEntityDescription[Robot](
(FeederRobot, LitterRobot3, LitterRobot4): (
RobotBinarySensorEntityDescription[FeederRobot | LitterRobot3 | LitterRobot4](
key="power_status",
translation_key="power_status",
device_class=BinarySensorDeviceClass.PLUG,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
is_on_fn=lambda robot: robot.power_status == "AC",
is_on_fn=lambda robot: robot.power_type == "AC",
),
),
}
@@ -16,5 +16,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "platinum",
"requirements": ["pylitterbot==2025.3.2"]
"requirements": ["pylitterbot==2025.4.0"]
}
+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),
}
+8 -2
View File
@@ -253,8 +253,10 @@ class MatterFan(MatterEntity, FanEntity):
return
self._feature_map = feature_map
self._attr_supported_features = FanEntityFeature(0)
# Reset to default so a featuremap change from MultiSpeed -> non-MultiSpeed
# does not leave a stale speed_count / percentage_step.
self._attr_speed_count = 100
if feature_map & FanControlFeature.kMultiSpeed:
self._attr_supported_features |= FanEntityFeature.SET_SPEED
self._attr_speed_count = int(
self.get_matter_attribute_value(clusters.FanControl.Attributes.SpeedMax)
)
@@ -304,8 +306,12 @@ class MatterFan(MatterEntity, FanEntity):
if feature_map & FanControlFeature.kAirflowDirection:
self._attr_supported_features |= FanEntityFeature.DIRECTION
# PercentSetting is always a mandatory attribute of the FanControl cluster,
# so percentage-based speed control is always available.
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
FanEntityFeature.SET_SPEED
| FanEntityFeature.TURN_OFF
| FanEntityFeature.TURN_ON
)
@@ -1,24 +1,98 @@
"""Provides conditions for media players."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.condition import Condition, make_entity_state_condition
from datetime import datetime
from typing import Any
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.condition import (
Condition,
EntityConditionBase,
EntityNumericalConditionBase,
make_entity_state_condition,
)
from . import ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED
from .const import DOMAIN, MediaPlayerState
class _MediaPlayerMutedConditionBase(EntityConditionBase):
"""Base class for media player is_muted/is_unmuted conditions."""
_domain_specs = {DOMAIN: DomainSpec()}
_target_muted: bool
def _state_valid_since(self, state: State) -> datetime:
"""Anchor `for:` durations to `last_updated` for the muted attribute.
Needed because the domain spec does not reflect that the condition
reads from the muted and volume attributes.
"""
return state.last_updated
def _has_volume_attributes(self, state: State) -> bool:
"""Check if the state has volume muted or volume level attributes."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is not None
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)
def _should_include(self, state: State) -> bool:
"""Skip entities without volume attributes from the all/count check."""
return super()._should_include(state) and self._has_volume_attributes(state)
def _is_muted(self, state: State) -> bool:
"""Check if the media player is muted."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0
)
def is_valid_state(self, entity_state: State) -> bool:
"""Check if the entity state matches the targeted muted state."""
if not self._has_volume_attributes(entity_state):
return False
return self._is_muted(entity_state) is self._target_muted
class MediaPlayerIsMutedCondition(_MediaPlayerMutedConditionBase):
"""Condition that passes when the media player is muted."""
_target_muted = True
class MediaPlayerIsUnmutedCondition(_MediaPlayerMutedConditionBase):
"""Condition that passes when the media player is not muted."""
_target_muted = False
class MediaPlayerIsVolumeCondition(EntityNumericalConditionBase):
"""Condition for media player volume level with 0.0-1.0 to percentage conversion."""
_domain_specs = {DOMAIN: DomainSpec(value_source=ATTR_MEDIA_VOLUME_LEVEL)}
_valid_unit = "%"
def _get_tracked_value(self, entity_state: State) -> Any:
"""Get the volume value converted from 0.0-1.0 to percentage (0-100)."""
raw = super()._get_tracked_value(entity_state)
if raw is None:
return None
try:
return float(raw) * 100.0
except TypeError, ValueError:
return None
def _should_include(self, state: State) -> bool:
"""Skip media players that do not expose a volume_level attribute."""
return (
super()._should_include(state)
and state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)
CONDITIONS: dict[str, type[Condition]] = {
"is_off": make_entity_state_condition(
DOMAIN, MediaPlayerState.OFF, support_duration=True
),
"is_on": make_entity_state_condition(
DOMAIN,
{
MediaPlayerState.BUFFERING,
MediaPlayerState.IDLE,
MediaPlayerState.ON,
MediaPlayerState.PAUSED,
MediaPlayerState.PLAYING,
},
),
"is_muted": MediaPlayerIsMutedCondition,
"is_not_playing": make_entity_state_condition(
DOMAIN,
{
@@ -29,12 +103,21 @@ 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_off": make_entity_state_condition(DOMAIN, MediaPlayerState.OFF),
"is_on": make_entity_state_condition(
DOMAIN,
{
MediaPlayerState.BUFFERING,
MediaPlayerState.IDLE,
MediaPlayerState.ON,
MediaPlayerState.PAUSED,
MediaPlayerState.PLAYING,
},
),
"is_paused": make_entity_state_condition(DOMAIN, MediaPlayerState.PAUSED),
"is_playing": make_entity_state_condition(DOMAIN, MediaPlayerState.PLAYING),
"is_unmuted": MediaPlayerIsUnmutedCondition,
"is_volume": MediaPlayerIsVolumeCondition,
}
@@ -9,19 +9,43 @@
selector:
automation_behavior:
mode: condition
.condition_common_with_for: &condition_common_with_for
target: *condition_media_player_target
fields:
behavior: *condition_behavior
for:
for: &condition_for
required: true
default: 00:00:00
selector:
duration:
is_off: *condition_common_with_for
.volume_threshold_entity: &volume_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: number
unit_of_measurement: "%"
- domain: sensor
unit_of_measurement: "%"
.volume_threshold_number: &volume_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"
is_muted: *condition_common
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
is_unmuted: *condition_common
is_volume:
target: *condition_media_player_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
numeric_threshold:
entity: *volume_threshold_entity
mode: is
number: *volume_threshold_number
@@ -1,5 +1,8 @@
{
"conditions": {
"is_muted": {
"condition": "mdi:volume-mute"
},
"is_not_playing": {
"condition": "mdi:stop"
},
@@ -14,6 +17,12 @@
},
"is_playing": {
"condition": "mdi:play"
},
"is_unmuted": {
"condition": "mdi:volume-high"
},
"is_volume": {
"condition": "mdi:volume-medium"
}
},
"entity_component": {
@@ -123,6 +132,9 @@
}
},
"triggers": {
"muted": {
"trigger": "mdi:volume-mute"
},
"paused_playing": {
"trigger": "mdi:pause"
},
@@ -137,6 +149,15 @@
},
"turned_on": {
"trigger": "mdi:power"
},
"unmuted": {
"trigger": "mdi:volume-high"
},
"volume_changed": {
"trigger": "mdi:volume-medium"
},
"volume_crossed_threshold": {
"trigger": "mdi:volume-medium"
}
}
}
@@ -2,15 +2,32 @@
"common": {
"condition_behavior_name": "Condition passes if",
"condition_for_name": "For at least",
"condition_threshold_name": "Threshold",
"trigger_behavior_name": "Trigger when",
"trigger_for_name": "For at least"
"trigger_for_name": "For at least",
"trigger_threshold_name": "Threshold"
},
"conditions": {
"is_muted": {
"description": "Tests if one or more media players are muted.",
"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 muted"
},
"is_not_playing": {
"description": "Tests if one or more media players are not playing.",
"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 +49,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"
@@ -59,6 +79,33 @@
}
},
"name": "Media player is playing"
},
"is_unmuted": {
"description": "Tests if one or more media players are not muted.",
"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 muted"
},
"is_volume": {
"description": "Tests the volume of one or more media players.",
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::condition_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::condition_for_name%]"
},
"threshold": {
"name": "[%key:component::media_player::common::condition_threshold_name%]"
}
},
"name": "Volume"
}
},
"device_automation": {
@@ -431,6 +478,18 @@
},
"title": "Media player",
"triggers": {
"muted": {
"description": "Triggers after one or more media players are muted.",
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player muted"
},
"paused_playing": {
"description": "Triggers after one or more media players pause playing.",
"fields": {
@@ -490,6 +549,42 @@
}
},
"name": "Media player turned on"
},
"unmuted": {
"description": "Triggers after one or more media players are unmuted.",
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
}
},
"name": "Media player unmuted"
},
"volume_changed": {
"description": "Triggers after the volume of one or more media players changes.",
"fields": {
"threshold": {
"name": "[%key:component::media_player::common::trigger_threshold_name%]"
}
},
"name": "Media player volume changed"
},
"volume_crossed_threshold": {
"description": "Triggers after the volume of one or more media players crosses a threshold.",
"fields": {
"behavior": {
"name": "[%key:component::media_player::common::trigger_behavior_name%]"
},
"for": {
"name": "[%key:component::media_player::common::trigger_for_name%]"
},
"threshold": {
"name": "[%key:component::media_player::common::trigger_threshold_name%]"
}
},
"name": "Media player volume crossed threshold"
}
}
}
@@ -1,12 +1,125 @@
"""Provides triggers for media players."""
from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_transition_trigger
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State
from homeassistant.helpers.automation import DomainSpec
from homeassistant.helpers.trigger import (
EntityNumericalStateChangedTriggerBase,
EntityNumericalStateCrossedThresholdTriggerBase,
EntityNumericalStateTriggerBase,
EntityTriggerBase,
Trigger,
make_entity_transition_trigger,
)
from . import MediaPlayerState
from . import ATTR_MEDIA_VOLUME_LEVEL, ATTR_MEDIA_VOLUME_MUTED, MediaPlayerState
from .const import DOMAIN
VOLUME_DOMAIN_SPECS = {
DOMAIN: DomainSpec(value_source=ATTR_MEDIA_VOLUME_LEVEL),
}
class _MediaPlayerMutedStateTriggerBase(EntityTriggerBase):
"""Base class for media player muted/unmuted triggers."""
_domain_specs = {DOMAIN: DomainSpec()}
_target_muted: bool
def _has_volume_attributes(self, state: State) -> bool:
"""Check if the state has volume muted or volume level attributes."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is not None
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)
def _should_include(self, state: State) -> bool:
"""Check if an entity should participate in all/count checks.
Entities without volume attributes cannot be muted, so they are
excluded from the check - otherwise an "all" check would never
pass when there are media players without volume support.
"""
return super()._should_include(state) and self._has_volume_attributes(state)
def is_muted(self, state: State) -> bool:
"""Check if the media player is muted."""
return (
state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) is True
or state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) == 0
)
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
"""Check if the origin state is valid and the state has changed."""
if from_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
if not self._has_volume_attributes(to_state):
return False
return self.is_muted(from_state) != self.is_muted(to_state)
def is_valid_state(self, state: State) -> bool:
"""Check if the new state matches the expected state."""
if not self._has_volume_attributes(state):
return False
return self.is_muted(state) is self._target_muted
class MediaPlayerMutedTrigger(_MediaPlayerMutedStateTriggerBase):
"""Class for media player muted triggers."""
_target_muted = True
class MediaPlayerUnmutedTrigger(_MediaPlayerMutedStateTriggerBase):
"""Class for media player unmuted triggers."""
_target_muted = False
class VolumeTriggerMixin(EntityNumericalStateTriggerBase):
"""Mixin for volume triggers."""
_domain_specs = VOLUME_DOMAIN_SPECS
_valid_unit = "%"
def _get_tracked_value(self, state: State) -> float | None:
"""Get tracked volume as a percentage."""
value = super()._get_tracked_value(state)
if value is None:
return None
# Convert 0.0-1.0 range to percentage (0-100)
return value * 100.0
def _should_include(self, state: State) -> bool:
"""Check if an entity should participate in all/count checks.
Entities without a volume level cannot have their volume tracked,
so they are excluded - otherwise an "all" check would never pass
when there are media players without volume support.
"""
return (
super()._should_include(state)
and state.attributes.get(ATTR_MEDIA_VOLUME_LEVEL) is not None
)
class VolumeChangedTrigger(EntityNumericalStateChangedTriggerBase, VolumeTriggerMixin):
"""Trigger for media player volume changes."""
class VolumeCrossedThresholdTrigger(
EntityNumericalStateCrossedThresholdTriggerBase, VolumeTriggerMixin
):
"""Trigger for media player volume crossing a threshold."""
TRIGGERS: dict[str, type[Trigger]] = {
"muted": MediaPlayerMutedTrigger,
"unmuted": MediaPlayerUnmutedTrigger,
"volume_changed": VolumeChangedTrigger,
"volume_crossed_threshold": VolumeCrossedThresholdTrigger,
"paused_playing": make_entity_transition_trigger(
DOMAIN,
from_states={
@@ -1,22 +1,62 @@
.trigger_common: &trigger_common
target:
target: &trigger_media_player_target
entity:
domain: media_player
fields:
behavior:
behavior: &trigger_behavior
required: true
default: any
selector:
automation_behavior:
mode: trigger
for:
for: &trigger_for
required: true
default: 00:00:00
selector:
duration:
.volume_threshold_entity: &volume_threshold_entity
- domain: input_number
unit_of_measurement: "%"
- domain: number
unit_of_measurement: "%"
- domain: sensor
unit_of_measurement: "%"
.volume_threshold_number: &volume_threshold_number
min: 0
max: 100
mode: box
unit_of_measurement: "%"
muted: *trigger_common
unmuted: *trigger_common
paused_playing: *trigger_common
started_playing: *trigger_common
stopped_playing: *trigger_common
turned_off: *trigger_common
turned_on: *trigger_common
volume_changed:
target: *trigger_media_player_target
fields:
threshold:
required: true
selector:
numeric_threshold:
entity: *volume_threshold_entity
mode: changed
number: *volume_threshold_number
volume_crossed_threshold:
target: *trigger_media_player_target
fields:
behavior: *trigger_behavior
for: *trigger_for
threshold:
required: true
selector:
numeric_threshold:
entity: *volume_threshold_entity
mode: crossed
number: *volume_threshold_number
+1
View File
@@ -479,6 +479,7 @@ class WashingMachineProgramId(MieleEnum, missing_to_none=True):
down_filled_items = 129
cottons_eco = 133
quick_power_wash = 146, 10031
quick_intense = 177
eco_40_60 = 190, 10007
bed_linen = 10047
easy_care = 10016
@@ -82,6 +82,7 @@ ATTR_SENSOR_UOM = "unit_of_measurement"
SIGNAL_SENSOR_UPDATE = f"{DOMAIN}_sensor_update"
SIGNAL_LOCATION_UPDATE = DOMAIN + "_location_update_{}"
SIGNAL_RECORD_NOTIFICATION = f"{DOMAIN}_record_notification"
ATTR_CAMERA_ENTITY_ID = "camera_entity_id"
+23 -1
View File
@@ -23,9 +23,13 @@ from homeassistant.components.notify import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_DEVICE_ID
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
@@ -48,6 +52,7 @@ from .const import (
DATA_NOTIFY,
DATA_PUSH_CHANNEL,
DOMAIN,
SIGNAL_RECORD_NOTIFICATION,
)
from .helpers import device_info
from .push_notification import PushChannel
@@ -113,6 +118,21 @@ class MobileAppNotifyEntity(NotifyEntity):
translation_placeholders={"device_name": self._config_entry.title},
)
@callback
def _async_handle_notification(self, webhook_id: str) -> None:
"""Handle notifications triggered externally."""
if webhook_id == self._config_entry.data[ATTR_WEBHOOK_ID]:
self._async_record_notification()
async def async_added_to_hass(self) -> None:
"""Register callback."""
self.async_on_remove(
async_dispatcher_connect(
self.hass, SIGNAL_RECORD_NOTIFICATION, self._async_handle_notification
)
)
def push_registrations(hass: HomeAssistant) -> dict[str, str]:
"""Return a dictionary of push enabled registrations."""
@@ -197,6 +217,7 @@ class MobileAppNotificationService(BaseNotificationService):
data,
partial(self._async_send_remote_message_target, entry),
)
async_dispatcher_send(self.hass, SIGNAL_RECORD_NOTIFICATION, target)
continue
# Test if local push only.
@@ -205,6 +226,7 @@ class MobileAppNotificationService(BaseNotificationService):
continue
await self._async_send_remote_message_target(entry, data)
async_dispatcher_send(self.hass, SIGNAL_RECORD_NOTIFICATION, target)
if failed_targets:
raise HomeAssistantError(
@@ -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%]"
}
}
}
@@ -56,6 +56,8 @@ DATA_SCHEMA = vol.Schema(
async def test_api(hass: HomeAssistant, user_input: dict[str, Any]) -> dict[str, str]:
"""Test fetch data from Nord Pool."""
if not user_input.get(CONF_AREAS):
return {CONF_AREAS: "no_areas"}
client = NordPoolClient(async_get_clientsession(hass))
try:
await client.async_get_delivery_period(
@@ -5,6 +5,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_areas": "No area(s) selected",
"no_data": "API connected but the response was empty"
},
"step": {
@@ -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": {
@@ -4,7 +4,7 @@ from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal
from openai import OpenAIError
from propcache.api import cached_property
@@ -166,14 +166,15 @@ class OpenAITTSEntity(TextToSpeechEntity, OpenAIBaseLLMEntity):
client = self.entry.runtime_data
response_format = options[ATTR_PREFERRED_FORMAT]
if response_format not in self._supported_formats:
# common aliases
if response_format == "ogg":
response_format = "opus"
elif response_format == "raw":
response_format = "pcm"
else:
response_format = self.default_options[ATTR_PREFERRED_FORMAT]
if response_format in ("ogg", "oga"):
codec: Literal["mp3", "opus", "aac", "flac", "wav", "pcm"] = "opus"
elif response_format == "raw":
response_format = codec = "pcm"
elif response_format not in self._supported_formats:
response_format = self.default_options[ATTR_PREFERRED_FORMAT]
codec = response_format
else:
codec = response_format
try:
async with client.audio.speech.with_streaming_response.create(
@@ -182,7 +183,7 @@ class OpenAITTSEntity(TextToSpeechEntity, OpenAIBaseLLMEntity):
input=message,
instructions=str(options.get(CONF_PROMPT)),
speed=options.get(CONF_TTS_SPEED, RECOMMENDED_TTS_SPEED),
response_format=response_format,
response_format=codec,
) as response:
response_data = bytearray()
async for chunk in response.iter_bytes():
@@ -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.1"]
}
@@ -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,
+6 -6
View File
@@ -768,14 +768,14 @@ class ScriptEntity(BaseScriptEntity, RestoreEntity):
async def async_will_remove_from_hass(self) -> None:
"""Stop script and remove service when it will be removed from HA."""
await self.script.async_stop()
if not self.registry_entry or self.registry_entry.entity_id == self.entity_id:
# Entity ID not changed, unload the script as it will not be reused.
self.script.async_unload()
# remove service
self.hass.services.async_remove(DOMAIN, self._attr_unique_id)
if self.registry_entry and self.registry_entry.entity_id != self.entity_id:
# Entity ID change, do not unload the script as it will be reused.
await self.script.async_stop()
return
await self.script.async_unload()
@websocket_api.websocket_command({"type": "script/config", "entity_id": str})
def websocket_config(

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