Compare commits

...

277 Commits

Author SHA1 Message Date
Franck Nijhof c9ed57bc56 2026.5.3 (#171185) 2026-05-19 11:49:12 +02:00
bkobus-bbx 0e0901993d Fix blebox light temperature scaling (#170573) 2026-05-19 08:47:23 +00:00
Franck Nijhof 54aba11091 Bump version to 2026.5.3 2026-05-19 08:39:50 +00:00
Mick Vleeshouwer dc9116a7a7 Fix tilt and position support for VenetianBlind covers in Overkiz (#170974) 2026-05-19 08:39:38 +00:00
Mick Vleeshouwer 1e90882918 Fix is_closed state and position for DynamicPergola covers in Overkiz (#170983) 2026-05-19 08:37:54 +00:00
puddly e8295e14b1 Fix ZHA config entries using a URI without a port (#171164) 2026-05-19 08:35:43 +00:00
Mick Vleeshouwer 7ebaaf129a Fix controls for UpDownGarageDoor4T and additional 4T covers in Overkiz (#171144) 2026-05-19 08:35:00 +00:00
Michael ee734dede6 Bump aioimmich to 0.14.1 (#171138) 2026-05-19 08:33:58 +00:00
Franck Nijhof ebc582c813 Return media_content_id as string in forked_daapd (#171059) 2026-05-19 08:33:56 +00:00
James Nimmo 311e5a9bd2 Bump pyIntesishome to 1.8.8 (#171041) 2026-05-19 08:33:54 +00:00
Franck Nijhof cd6c3c878b Fix WeatherFlow websocket crash when data payload is None (#171037) 2026-05-19 08:33:52 +00:00
Franck Nijhof 51589ec2ff Add stop command to Overkiz pergola horizontal awning covers (#171034) 2026-05-19 08:33:50 +00:00
Franck Nijhof 8e1a04dc82 Fix Verisure alarm crash when cloud rejects arm/disarm command (#171024) 2026-05-19 08:33:48 +00:00
Mick Vleeshouwer 6b15f9a2ec Add additional overrides to cover entity in Overkiz (#171019) 2026-05-19 08:33:46 +00:00
Franck Nijhof 8d66752556 Fix shorthand template conditions in choose blocks crashing all automations (#171018) 2026-05-19 08:33:44 +00:00
Franck Nijhof 266767e37d Handle Daikin connection errors gracefully in coordinator (#171017) 2026-05-19 08:33:42 +00:00
Franck Nijhof d39775ac34 Fix manual alarm panel crash on restore with invalid state (#171016) 2026-05-19 08:33:40 +00:00
Franck Nijhof a314f7bf64 Fix Control4 climate crash when humidity is 'Undefined' (#171015) 2026-05-19 08:33:38 +00:00
Franck Nijhof 37478d33eb Fix SleepIQ timer units: seconds should be minutes for core climate and foot warmer (#171013) 2026-05-19 08:33:36 +00:00
Franck Nijhof 5a76f3bd19 Fix Growatt mix device IndexError when chart data is empty (#171012) 2026-05-19 08:33:34 +00:00
Franck Nijhof 17e105083e Fix threshold preview crash when hysteresis is not provided (#171009) 2026-05-19 08:33:32 +00:00
Franck Nijhof db8589b2bc Fix time trigger crash when using entity_id dict format without offset (#171006)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-19 08:33:30 +00:00
Franck Nijhof 771b016f33 Fix Netatmo valve KeyError when hvac_action state is unavailable in Overkiz (#171004) 2026-05-19 08:33:28 +00:00
Franck Nijhof 0bc0745e8c Use asyncio.get_running_loop() in emulated_hue UPnP responder (#171000)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:43 +00:00
Franck Nijhof ea084797d3 Load template extensions by class to prevent import deadlock (#170995)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:41 +00:00
Franck Nijhof 2456753caf Prevent Google Assistant entity sync from blocking startup (#170991)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:31:39 +00:00
Mick Vleeshouwer 070de13c14 Fix controls for OpenCloseGate4T (rts:GateOpenerRTS4TComponent) in Overkiz (#170987) 2026-05-19 08:30:30 +00:00
Mick Vleeshouwer 5e45f37ee6 Fix is_closed state for DiscretePositionableGarageDoor in Overkiz (#170981) 2026-05-19 08:25:10 +00:00
Franck Nijhof 4a96880f51 Reduce GoodWe connect retries to avoid blocking startup (#170964)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:26 +00:00
Franck Nijhof 228ac01124 Use correct state_class for utility meters with device classes that don't support total_increasing (#170962)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:24 +00:00
Franck Nijhof d366027e6b Fix utility meter next_reset shifting forward on entity rename (#170957)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 08:20:22 +00:00
puddly 2f35ad2a8a Disable USB discovery for teleinfo (#170933) 2026-05-19 08:20:20 +00:00
Mick Vleeshouwer 95cc9aed64 Fix is_closed state for SlidingDiscreteGateWithPedestrianPosition covers in Overkiz (#170913) 2026-05-19 08:19:03 +00:00
Franck Nijhof 37d6449a49 Populate uid and recurrence_id in CalDAV calendar events (#170910) 2026-05-19 08:14:10 +00:00
J. Nick Koston 249b5435d9 Bump aiodns to 4.0.3 (#170865)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-19 08:07:21 +00:00
bkobus-bbx 3293ebcea5 Fix ValueError when turning on blebox light with brightness set to 0 (#170769)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-19 08:07:18 +00:00
Daniil Karpenko 47d8adc77c Add tilt controls for UpDownSheerScreen in Overkiz (#170563)
Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
2026-05-19 08:01:00 +00:00
Keith Roehrenbeck 356e6a691b Fix Apple TV keyboard focus binary_sensor missing on cold start (#170360) 2026-05-19 08:00:58 +00:00
Florent Thoumie b26c2f3854 Improve iaqualink 429 handling (#170231) 2026-05-19 08:00:56 +00:00
Luka Matijević 0830988687 Bump qbittorrent-api to 2026.5.1 (#170181) 2026-05-19 08:00:54 +00:00
Franck Nijhof 456202325a 2026.5.2 (#170840) 2026-05-15 22:55:45 +02:00
Franck Nijhof 1e47149764 Fix hassfest warning 2026-05-15 20:26:51 +00:00
Franck Nijhof 116b63ca3a Bump version to 2026.5.2 2026-05-15 20:13:00 +00:00
Ronald van der Meer 3096bcf8a9 Bump python-duco-connectivity to 0.4.0 (#170661) 2026-05-15 20:12:26 +00:00
Ronald van der Meer a4027029d0 Migrate Duco to python-duco-connectivity and remove temperature sensors (#170237) 2026-05-15 20:11:35 +00:00
Bram Kragten fffc9d0695 Update frontend to 20260429.4 (#170567) 2026-05-15 20:06:23 +00:00
G Johansson 3ca5cf5add Add missing optional category strings in workday (#170505)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 20:06:21 +00:00
Jan Bouwhuis 087cb77042 Fix MQTT settings in device subentry device settings are not recalled when reconfiguring the device (#170484) 2026-05-15 20:06:19 +00:00
Michael Keck 8bd1c07ec9 Increase WebDAV client timeout from 10 to 30 seconds (#170476) 2026-05-15 20:06:17 +00:00
J. Nick Koston 9ecb59590b Bump aioharmony to 1.0.3 (#170459) 2026-05-15 20:02:46 +00:00
Rob Bierbooms e14eb9fbc5 Fix influxdb reconfigure for v1 configuration (#170448) 2026-05-15 20:01:59 +00:00
TheJulianJES 149c796227 Fix fractional setpoints in Matter climate not rounded (#170442) 2026-05-15 20:01:11 +00:00
J. Nick Koston 3383e5b1e9 Bump aioesphomeapi to 44.24.1 (#170428) 2026-05-15 20:00:24 +00:00
Åke Strandberg 05862c6dc8 Bump pymiele version to 0.6.2 (#170419) 2026-05-15 19:59:37 +00:00
Petar Petrov b35ac41470 Apply unit_of_measurement to energy combined power sensor (#170404) 2026-05-15 19:58:50 +00:00
James Nimmo 20cec56512 Bump pyintesishome to 1.8.7 (#170382) 2026-05-15 19:58:03 +00:00
puddly 74580262b6 Bump serialx to 1.7.3 (#170368) 2026-05-15 19:57:16 +00:00
Pascal Brunot f75cdae602 Bump serialx to 1.7.2 (#170272) 2026-05-15 19:56:59 +00:00
Jan Bouwhuis 8c95f4f7ae Fix duplicate doorbell events when entity becomes unavailable (#170354)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 19:54:02 +00:00
Robert Svensson c3ec51c471 Bump axis to v71 (#170347) 2026-05-15 19:54:00 +00:00
Raman Gupta 0f80a4bc18 Cancel previous Debouncer timer handle in _schedule_timer (#170339)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 19:53:58 +00:00
Maciej Bieniek 0761d618f1 Fix Shelly media player availability (#170319) 2026-05-15 19:53:57 +00:00
Stefan Agner 03e3c46faf Fix hassio.backup_partial AttributeError when folders are specified (#170312)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 19:53:55 +00:00
Craig Dean d1962b0df2 Bump renault-api to 0.5.8 (#170309) 2026-05-15 19:53:53 +00:00
Florent Thoumie 7a38a2303a iaqualink: set system specific polling interval (#170279) 2026-05-15 19:53:51 +00:00
Maciej Bieniek 6f5c2a8614 Bump imgw-pib to 2.1.2 (#170274) 2026-05-15 19:53:49 +00:00
Sören Beye ff36498698 fix: Do not forget segments from state when a new config arrives (#170265)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 19:53:47 +00:00
Willem-Jan van Rootselaar 23e19ea2e4 Handle empty BSB-LAN heating circuits (#170249) 2026-05-15 19:53:46 +00:00
Ronald van der Meer c33f174041 Bump python-duco-client to 0.5.0 (#170065) 2026-05-15 19:52:32 +00:00
Ronald van der Meer bbe64d74e3 Bump python-duco-client to 0.4.2 (#170027) 2026-05-15 19:52:30 +00:00
Ronald van der Meer ed3a71f2ee Add API version to Duco diagnostics for support triage (#169802) 2026-05-15 19:51:21 +00:00
Ronald van der Meer 46c49daba4 Add system health platform for Duco integration (#169517) 2026-05-15 19:48:52 +00:00
Ronald van der Meer a2f2ded188 Add target flow level and mode end time sensors to Duco integration (#169298) 2026-05-15 19:47:15 +00:00
Simone Chemelli 7be061796d Fix entities refresh for UptimeRobot (#170217) 2026-05-15 19:32:16 +00:00
Jan Bouwhuis 27c7d8de0c Fix MQTT device discovery not using shared QoS and encoding options (#170195)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-15 19:32:14 +00:00
Simone Chemelli 07542523b5 Reinit API on stale session for Vodafone Station (#170190) 2026-05-15 19:32:12 +00:00
puddly 18597bb653 Set serial port description from description, not product (#170160)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2026-05-15 19:32:10 +00:00
Christian Lackas c4be57a294 homematicip_cloud: fix HmIP-FLC lock state polarity (#170159) 2026-05-15 19:32:08 +00:00
Christian Lackas 7ceaebb086 Fix homematicip_cloud config entry setup crash after migration to 2026.5.0 (#170156) 2026-05-15 19:32:06 +00:00
Mick Vleeshouwer 7c5ef09734 Fix local API incorrectly marking devices as unavailable in Overkiz (#170118)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2026-05-15 19:32:05 +00:00
Thijs W. b4d8ba66fe Update afsapi to 1.0.1 (#170073) 2026-05-15 19:32:02 +00:00
puddly 308221ce67 Migrate ZBT-1 and ZBT-2 to use serial number for unique_id (#169879)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-05-15 19:30:56 +00:00
Simone Chemelli 1344213335 Fix non unique_id for Comelit (#169756)
Co-authored-by: Copilot <copilot@github.com>
2026-05-15 19:26:54 +00:00
r2xj 7e405e9014 Only use SmartThings switch for light if it should (#166424)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-05-15 19:26:52 +00:00
LG-ThinQ-Integration b0c45132ed Fix ValueError for non-numeric value in LG ThinQ (#166300)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-05-15 19:26:49 +00:00
Franck Nijhof 7d7738303a 2026.5.1 (#170146) 2026-05-08 22:07:51 +02:00
Franck Nijhof dd0cdc4fc4 Bump version to 2026.5.1 2026-05-08 18:54:08 +00:00
Mick Vleeshouwer 18ea40c46d Fix tilt support for UpDownVenetianBlind (rts:VenetianBlindRTSComponent) in Overkiz (#170047) 2026-05-08 18:53:57 +00:00
Mick Vleeshouwer a23131efc8 Fix is_closed state for DynamicGate covers in Overkiz (#170130) 2026-05-08 18:53:10 +00:00
bkobus-bbx 4940a0abae Bump blebox_uniapi to v2.5.3 (#170115) 2026-05-08 18:53:08 +00:00
Willem-Jan van Rootselaar 5f98d5ae52 Bump python-bsblan to 5.2.1 (#170100) 2026-05-08 18:53:06 +00:00
TheJulianJES ba18cded30 Bump ZHA to 1.3.1 (#170095) 2026-05-08 18:53:04 +00:00
TheJulianJES fb7504e9df Fix Z-Wave discovery crash with unknown node firmware version (#170090) 2026-05-08 18:53:02 +00:00
Mick Vleeshouwer 106f815a1e Fix sensors getting wrong unit from MeasuredValueType attribute in Overkiz (#170088) 2026-05-08 18:53:00 +00:00
Mick Vleeshouwer 167757762b Set is_closed state to None when a cover state returns "unknown" in Overkiz (#170081) 2026-05-08 18:52:58 +00:00
Robert Resch 3a902e1a16 Bump deebot-client to 18.3.0 (#170066) 2026-05-08 18:52:56 +00:00
Mick Vleeshouwer 85c11672d8 Bump pyOverkiz to 1.20.3 (#170060) 2026-05-08 18:52:54 +00:00
Mick Vleeshouwer 89649df20d Fix cover controls for UpDownBioclimaticPergola in Overkiz (#170058) 2026-05-08 18:52:52 +00:00
Mick Vleeshouwer 7b749b95ce Fix tilt controls for TiltOnlyVenetianBlind in Overkiz (#170055)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-08 18:52:50 +00:00
Mick Vleeshouwer cc140be85c Fix is_closed state for DynamicGarageDoor in Overkiz (#170052) 2026-05-08 18:52:47 +00:00
Robert Svensson e1ad765414 Fix websocket certificate verification Bump axis to v70 (#170038) 2026-05-08 18:48:55 +00:00
Michael 44b1fea745 Proper handling of malformed data during FRITZ!Box Tools setup (#170030) 2026-05-08 18:48:54 +00:00
Ronald van der Meer 5dd04363b2 Bump python-duco-client to 0.4.1 (#169991) 2026-05-08 18:48:51 +00:00
Ronald van der Meer 03aa979309 Bump python-duco-client to 0.4.0 (#169776) 2026-05-08 18:48:49 +00:00
Daniel Hjelseth Høyer 6fabbb354b Bump pyTibber to 0.37.5 (#169981)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-05-08 18:45:50 +00:00
Erik Montnemery f644448d0f Add support for options to todo triggers (#169947) 2026-05-08 18:45:48 +00:00
G Johansson 4e61581cd8 Bump holidays to 0.96 (#169939) 2026-05-08 18:45:47 +00:00
puddly 6f87d02b72 Bump serialx to 1.7.1 (#169928) 2026-05-08 18:45:45 +00:00
Joakim Plate 348f6149b4 Update gardena ble to 2.8.1 (#169914) 2026-05-08 18:45:43 +00:00
Stefan Agner a4227ef1bc Fix hassio auth IndexError on Supervisor Unix socket requests (#169911) 2026-05-08 18:45:41 +00:00
Jeef aac49a567f Fix IntelliFire setup recovery (#169739) 2026-05-08 18:45:39 +00:00
Rob Treacy 76b878b136 Fix WiZ Light config flow timeout by properly closing UDP connections (#168456) 2026-05-08 18:45:37 +00:00
th3spis 2d05931683 Added wfsens as a occupancy source in wiz (#166799)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-05-08 18:45:35 +00:00
Franck Nijhof b10582b0a9 2026.5.0 (#169484) 2026-05-06 17:22:09 +02:00
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
Joakim Plate f5363db26f Move finish watering to sensor (#169476)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 17:34:38 +02:00
Erik Montnemery 3be1aa5441 Include errors in script trace when continue_on_error is set (#168676) 2026-04-29 17:30:47 +02:00
Paul Bottein 7dbffb7375 Update frontend to 20260429.0 (#169475) 2026-04-29 17:21:40 +02:00
A. Gideonse 07c4025d47 Add indevolt binary sensor platform (#169375)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 16:19:17 +01:00
Franck Nijhof 3e3e425aa5 Bump Fumis integration to platinum quality scale (#169443) 2026-04-29 17:14:50 +02:00
Erik Montnemery 162a4fc385 Use automation behavior selector in triggers and conditions (#169438) 2026-04-29 17:10:50 +02:00
Manu ef6fd92079 Add notify entities to Mobile app integration (#168510)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-29 17:06:13 +02:00
Erik Montnemery 4ad71a070a Improve timer icons (#169474) 2026-04-29 17:02:22 +02:00
Erik Montnemery f33ad12f5e Correct entity_id change for automations (#169470) 2026-04-29 16:30:02 +02:00
Erik Montnemery da7fbb0dd6 Correct entity_id change for scripts (#169472) 2026-04-29 16:29:25 +02:00
Abílio Costa 81137345a3 Extract triggers/conditions/services for non-primary entities (#169441) 2026-04-29 15:28:09 +01:00
Erik Montnemery d3e77d4195 Add timer triggers (#169450) 2026-04-29 16:27:52 +02:00
renovate[bot] ce977e90a5 Update cryptography to 47.0.0 (#169465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 16:22:16 +02:00
Martin Hjelmare 2871b87344 Revert "Include indirect automation references in device view (#167719)" (#169471) 2026-04-29 15:15:50 +01:00
renovate[bot] d82ce1e22d Update ruff (#169473)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 16:11:22 +02:00
Guido Schmitz b8bb2e0090 Use uptime sensor class in devolo Home Network (#169469) 2026-04-29 16:10:39 +02:00
Erik Montnemery 1b81cfe3ca Make it always optional to specify trigger and condition options (#169467) 2026-04-29 15:06:05 +01:00
renovate[bot] 0a3f0d90c3 Update url-normalize to 3.0.0 (#169466)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:48:05 +02:00
renovate[bot] 84d566a02c Update pyOpenSSL to 26.1.0 (#169464)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:47:52 +02:00
renovate[bot] 0e0d54e4b6 Update uv to 0.11.8 (#169463)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:47:39 +02:00
epenet 5b05061def Fix plex sensor test broken by Python 3.14.3 asyncio changes (#169448) 2026-04-29 15:22:34 +02:00
renovate[bot] e0bf76769a Update ruff (#169461)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:17:04 +02:00
renovate[bot] 63868bc169 Migrate Renovate config (#169462)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 15:16:26 +02:00
Bram Kragten b8b7169371 Add automation behavior selector (#166484)
Co-authored-by: Erik <erik@montnemery.com>
2026-04-29 15:10:47 +02:00
Maciej Bieniek 1cc778954f Use new UPTIME sensor class in Brother (#169457) 2026-04-29 14:54:55 +02:00
Maciej Bieniek 3ba3ecdef3 Use new UPTIME sensor class in NAM (#169458) 2026-04-29 14:54:14 +02:00
Ronald van der Meer 5c57fc6e14 Fix Duco HTTPS polling performance by lowering SCAN_INTERVAL to 10 seconds (#169453) 2026-04-29 14:25:04 +02:00
epenet 2da440043a Fix Sonos group regroup race when entity is not yet registered (#169445) 2026-04-29 14:13:59 +02:00
epenet 4f34725e53 Fix flaky portainer test_device_registry (#169456)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:11:07 +02:00
epenet d03bec2f44 Fix race in devolo Home Network device tracker device lookup (#169454)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:08:00 +02:00
epenet 57c37fc10c Fix race in Ping device tracker device lookup (#169432)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 14:02:37 +02:00
Simone Chemelli fd98594143 Use defaults for device class UPTIME in Fritz (#169149) 2026-04-29 12:34:25 +01:00
Robert Svensson 894547abed Add Axis doorbell event platform (#169422) 2026-04-29 12:29:58 +01:00
Luis Miranda b48060674c Add OMIE integration (#150399)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
Co-authored-by: abmantis <amfcalt@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-29 12:10:22 +01:00
vturekhanov 6f2aa7852a Fix availability state for bridged Matter devices (#165078)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 13:05:15 +02:00
Abílio Costa 9d53645468 Remove LLM test instruction (#169442) 2026-04-29 12:58:47 +02:00
Simone Chemelli a3f1c067f7 Fix host connections for Fritz (#169434) 2026-04-29 12:57:05 +02:00
Tomer cef97973d0 Victron GX device_tracker optional attributes (#168646)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 12:56:13 +02:00
TheJulianJES 7bb297a3fc Bump ZHA to 1.3.0 (#169433) 2026-04-29 12:51:18 +02:00
Maciej Bieniek 7e2b8e1a48 Bump aioshelly to 13.24.2 (#169440) 2026-04-29 12:50:44 +02:00
Franck Nijhof 013c5e7f7c Add diagnostics to Fumis integration (#169437) 2026-04-29 12:38:09 +02:00
Abílio Costa 7cb1d5b8ab Allow targeting non-primary entities in conditions (#169291) 2026-04-29 12:25:26 +02:00
Paulus Schoutsen 57d9e8ea6f Filter history API responses by per-entity read permissions (#169236)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:16:49 +02:00
Andrew Ng 32743fcf8d Fix Acaia battery sensor going unavailable on first-session disconnect (#169420)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-04-29 12:12:44 +02:00
Simone Chemelli f4637db26d Add routine management to Alexa Devices (#166291) 2026-04-29 11:45:03 +02:00
Erik Montnemery b4bfe6b80b Rename timer last_action to last_transition (#169430) 2026-04-29 11:35:36 +02:00
Andrej Walilko 278f25ec6e Redact sensitive api creds before logging message in websocket api (#169326)
Co-authored-by: Andrej Walilko <awalilko@liquidweb.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 11:15:05 +02:00
Robert Resch 39d3bc3e53 Bump deebot-client to 18.2.0 (#169003) 2026-04-29 11:13:14 +02:00
Yabing Yi bb41a2df9f Fix logbook spam by including image domain in ALWAYS_CONTINUOUS_DOMAINS (#169240)
Co-authored-by: Claude Code <claude@anthropic.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-04-29 10:58:13 +02:00
Petar Petrov 284242b90e Copy unit_of_measurement onto energy inverted power sensor (#169427) 2026-04-29 10:56:08 +02:00
Erik Montnemery a95c216983 Unload scripts created by websocket command execute_script (#169368)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 10:24:24 +02:00
Simone Chemelli d41a3ae0cd Use defaults for device class UPTIME in Shelly (#169148) 2026-04-29 10:12:18 +02:00
J. Nick Koston 0dfbe3ef84 Expose async_clear_advertisement_history in the bluetooth API (#169191) 2026-04-29 10:11:27 +02:00
Franck Nijhof 71fc725d75 Extract state template functions into a state Jinja2 extension (#169034) 2026-04-29 10:03:38 +02:00
Shay Levy d41c9aee52 Bump aioshelly to 13.24.1 (#169426)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 10:53:38 +03:00
epenet 8091f511b8 Reject manifest dependencies on core integrations in hassfest (#169425) 2026-04-29 09:52:46 +02:00
Franck Nijhof a7baedc22b Add error and alert sensors to Fumis integration (#169307) 2026-04-29 09:51:22 +02:00
Franck Nijhof 05bfb3a52e Add number platform to Fumis integration (#169100) 2026-04-29 09:39:15 +02:00
Robert Resch 2a5b95ba4d Require hass in Template (#169292)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 09:26:32 +02:00
Steve Easley 3dd972cc7a Fix jvcprojector entities going unavailable on transient command errors (#168985) 2026-04-29 09:21:53 +02:00
Marc Mueller acd9dd218a Protect CI cache save against cancellation (#168310) 2026-04-29 09:20:37 +02:00
J. Diego Rodríguez Royo 6552cf8f7a Keep options values when chaging or starting program on Home Connect (#168575) 2026-04-29 09:19:41 +02:00
Artur Pragacz e4e4785225 Clean up entity_service_call tests (#169170)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 09:17:45 +02:00
G Johansson d531ce8d1d Use async_on_create_entry in bayesian (#169218)
Co-authored-by: Copilot <copilot@github.com>
2026-04-29 09:14:58 +02:00
Stefan Agner 0224928655 Bump python-otbr-api to 2.10.0 (#169370)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-04-29 09:10:26 +02:00
662 changed files with 26718 additions and 6489 deletions
@@ -15,7 +15,6 @@ description: Everything you need to know to build, test and review Home Assistan
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
@@ -18,7 +18,6 @@ excludeAgent: "cloud-agent"
- For entity actions and entity services, avoid requesting redundant defensive checks for fields already enforced by Home Assistant validation schemas and entity filters; only request extra guards when values bypass validation or are transformed unsafely.
- When validation guarantees a key is present, prefer direct dictionary indexing (`data["key"]`) over `.get("key")` so invalid assumptions fail fast.
- Integrations should be thin wrappers. Protocol parsing, device state machines, or other domain logic belong in a separate PyPI library, not in the integration itself. If unsure, ask before inlining.
- "potato" is a forbidden word for an integration and should never be used.
The following platforms have extra guidelines:
- **Diagnostics**: [`platform-diagnostics.md`](platform-diagnostics.md) for diagnostic data collection
+3 -2
View File
@@ -6,7 +6,7 @@
"pep621",
"pip_requirements",
"pre-commit",
"regex",
"custom.regex",
"homeassistant-manifest"
],
@@ -27,8 +27,9 @@
]
},
"regexManagers": [
"customManagers": [
{
"customType": "regex",
"description": "Update ruff required-version in pyproject.toml",
"managerFilePatterns": ["/^pyproject\\.toml$/"],
"matchStrings": ["required-version = \">=(?<currentValue>[\\d.]+)\""],
+29 -3
View File
@@ -366,7 +366,7 @@ jobs:
echo "key=uv-${UV_CACHE_VERSION}-${uv_version}-${HA_SHORT_VERSION}-$(date -u '+%Y-%m-%dT%H:%M:%s')" >> $GITHUB_OUTPUT
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
@@ -374,7 +374,8 @@ jobs:
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: cache-uv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
@@ -398,6 +399,7 @@ jobs:
if: |
steps.cache-venv.outputs.cache-hit != 'true'
|| steps.cache-apt-check.outputs.cache-hit != 'true'
id: install-os-deps
timeout-minutes: 10
env:
APT_CACHE_HIT: ${{ steps.cache-apt-check.outputs.cache-hit }}
@@ -431,7 +433,10 @@ jobs:
sudo chmod -R 755 ${APT_CACHE_BASE}
fi
- name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true'
if: |
always()
&& steps.cache-apt-check.outputs.cache-hit != 'true'
&& steps.install-os-deps.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
@@ -441,6 +446,7 @@ jobs:
${{ runner.os }}-${{ runner.arch }}-${{ needs.info.outputs.apt_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
id: create-venv
run: |
python -m venv venv
. venv/bin/activate
@@ -471,6 +477,26 @@ jobs:
- name: Check dirty
run: |
./script/check_dirty
- name: Save uv wheel cache
if: |
(success() && steps.cache-venv.outputs.cache-hit != 'true')
|| (always()
&& steps.create-venv.outcome == 'success'
&& steps.cache-uv.outputs.cache-matched-key == '')
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }}
- name: Save base Python virtual environment
if: always() && steps.create-venv.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
hassfest:
name: Check hassfest
+1 -1
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10
rev: v0.15.12
hooks:
- id: ruff-check
args:
Generated
+4 -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
@@ -1241,6 +1241,8 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/ollama/ @synesthesiam
/tests/components/ollama/ @synesthesiam
/homeassistant/components/ombi/ @larssont
/homeassistant/components/omie/ @luuuis
/tests/components/omie/ @luuuis
/homeassistant/components/onboarding/ @home-assistant/core
/tests/components/onboarding/ @home-assistant/core
/homeassistant/components/ondilo_ico/ @JeromeHXP
+16 -1
View File
@@ -2,7 +2,8 @@
from __future__ import annotations
from collections.abc import Callable
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING
import voluptuous as vol
@@ -13,6 +14,9 @@ from .models import PermissionLookup
from .types import PolicyType
from .util import test_all
if TYPE_CHECKING:
from ..models import User
POLICY_SCHEMA = vol.Schema({vol.Optional(CAT_ENTITIES): ENTITY_POLICY_SCHEMA})
__all__ = [
@@ -22,10 +26,21 @@ __all__ = [
"PermissionLookup",
"PolicyPermissions",
"PolicyType",
"filter_entity_ids_by_permission",
"merge_policies",
]
def filter_entity_ids_by_permission(
user: User, entity_ids: Iterable[str], key: str
) -> list[str]:
"""Filter entity IDs to those the user can access for the given policy key."""
if user.is_admin or user.permissions.access_all_entities(key):
return list(entity_ids)
check_entity = user.permissions.check_entity
return [entity_id for entity_id in entity_ids if check_entity(entity_id, key)]
class AbstractPermissions:
"""Default permissions class."""
+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"]
}
+1 -1
View File
@@ -143,4 +143,4 @@ class AcaiaRestoreSensor(AcaiaEntity, RestoreSensor):
@property
def available(self) -> bool:
"""Return True if entity is available."""
return super().available or self._restored_data is not None
return super().available or self.native_value is not None
@@ -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.3"]
}
@@ -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
)
@@ -4,11 +4,14 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
.condition_for: &condition_for
required: true
default: 00:00:00
selector:
duration:
# --- Unit lists for multi-unit pollutants ---
@@ -249,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
@@ -285,6 +284,7 @@ is_co_value:
target: *target_co_sensor
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -299,6 +299,7 @@ is_ozone_value:
target: *target_ozone
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -313,6 +314,7 @@ is_voc_value:
target: *target_voc
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -327,6 +329,7 @@ is_voc_ratio_value:
target: *target_voc_ratio
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -341,6 +344,7 @@ is_no_value:
target: *target_no
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -355,6 +359,7 @@ is_no2_value:
target: *target_no2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -369,6 +374,7 @@ is_so2_value:
target: *target_so2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -385,6 +391,7 @@ is_co2_value:
target: *target_co2
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -397,6 +404,7 @@ is_pm1_value:
target: *target_pm1
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -409,6 +417,7 @@ is_pm25_value:
target: *target_pm25
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -421,6 +430,7 @@ is_pm4_value:
target: *target_pm4
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -433,6 +443,7 @@ is_pm10_value:
target: *target_pm10
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -445,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%]"
}
@@ -237,21 +276,6 @@
"name": "Volatile organic compounds value"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Air Quality",
"triggers": {
"co2_changed": {
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -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,22 +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:
select:
translation_key: condition_behavior
options:
- all
- any
.condition_common_for: &condition_common_for
target: *condition_common_target
fields: &condition_common_for_fields
behavior: *condition_common_behavior
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -26,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
@@ -34,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
@@ -42,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
@@ -50,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"
@@ -160,21 +163,6 @@
"message": "Arming requires a code but none was given for {entity_id}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"alarm_arm_away": {
"description": "Arms an alarm in the away mode.",
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
+36 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from asyncio import timeout
from collections.abc import Mapping
from datetime import datetime, timedelta
from http import HTTPStatus
import json
import logging
@@ -13,7 +14,12 @@ from uuid import uuid4
import aiohttp
from homeassistant.components import event
from homeassistant.const import EVENT_STATE_CHANGED, STATE_ON
from homeassistant.const import (
EVENT_STATE_CHANGED,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
)
from homeassistant.core import (
CALLBACK_TYPE,
Event,
@@ -53,6 +59,25 @@ DEFAULT_TIMEOUT = 10
TO_REDACT = {"correlationToken", "token"}
def valid_doorbell_timestamp(entity_id: str, event_state: str) -> bool:
"""Check if doorbell event timestamp is valid."""
if event_state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
return False
try:
timestamp = datetime.fromisoformat(event_state)
except ValueError:
_LOGGER.debug(
"Unable to parse ISO timestamp from state for %s. Got %s",
entity_id,
event_state,
)
return False
else:
if (dt_util.utcnow() - timestamp) < timedelta(seconds=30):
return True
return False
class AlexaDirective:
"""An incoming Alexa directive."""
@@ -317,9 +342,17 @@ async def async_enable_proactive_mode(
if should_doorbell:
old_state = data["old_state"]
if new_state.domain == event.DOMAIN or (
if (
new_state.domain == event.DOMAIN
and valid_doorbell_timestamp(new_state.entity_id, new_state.state)
and (old_state is None or old_state.state != STATE_UNAVAILABLE)
and (old_state is None or old_state.state != new_state.state)
) or (
new_state.state == STATE_ON
and (old_state is None or old_state.state != STATE_ON)
and (
old_state is None
or old_state.state not in (STATE_ON, STATE_UNAVAILABLE)
)
):
await async_send_doorbell_event_message(
hass, smart_home_config, alexa_changed_entity
@@ -11,6 +11,7 @@ from .services import async_setup_services
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.NOTIFY,
Platform.SENSOR,
Platform.SWITCH,
@@ -0,0 +1,55 @@
"""Support for buttons."""
from homeassistant.components.button import ButtonEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import slugify
from .coordinator import AmazonConfigEntry, AmazonDevicesCoordinator
from .entity import AmazonServiceEntity
# Coordinator is used to centralize the data updates
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: AmazonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up button entities for Alexa Devices."""
coordinator = entry.runtime_data
known_routines: set[str] = set()
def _check_routines() -> None:
current_routines = set(coordinator.api.routines)
new_routines = current_routines - known_routines
if new_routines:
known_routines.update(new_routines)
async_add_entities(
AmazonRoutineButton(coordinator, routine) for routine in new_routines
)
_check_routines()
entry.async_on_unload(coordinator.async_add_listener(_check_routines))
class AmazonRoutineButton(AmazonServiceEntity, ButtonEntity):
"""Button entity for Alexa routine."""
_attr_has_entity_name = True
def __init__(self, coordinator: AmazonDevicesCoordinator, routine: str) -> None:
"""Initialize the routine button entity."""
self._coordinator = coordinator
self._routine = routine
super().__init__(
coordinator,
EntityDescription(key=slugify(routine), name=routine),
)
async def async_press(self) -> None:
"""Handle button press action."""
await self._coordinator.api.call_routine(self._routine)
@@ -12,12 +12,13 @@ from aioamazondevices.structures import AmazonDevice
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN
@@ -64,6 +65,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
for identifier_domain, identifier in device.identifiers
if identifier_domain == DOMAIN
}
self.previous_routines: set[str] = {
routine.unique_id
for routine in er.async_entries_for_config_entry(
er.async_get(hass), entry.entry_id
)
if routine.domain == Platform.BUTTON
}
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""
@@ -92,8 +100,13 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
current_devices = set(data.keys())
if stale_devices := self.previous_devices - current_devices:
await self._async_remove_device_stale(stale_devices)
self.previous_devices = current_devices
current_routines = {slugify(routine) for routine in self.api.routines}
if stale_routines := self.previous_routines - current_routines:
await self._async_remove_routine_stale(stale_routines)
self.previous_routines = current_routines
return data
async def _async_remove_device_stale(
@@ -116,3 +129,23 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
async def _async_remove_routine_stale(
self,
stale_routines: set[str],
) -> None:
"""Remove stale routine."""
entity_registry = er.async_get(self.hass)
for routine in stale_routines:
_LOGGER.debug(
"Detected change in routines: routine %s removed",
routine,
)
entity_id = entity_registry.async_get_entity_id(
Platform.BUTTON,
DOMAIN,
f"{slugify(self.config_entry.unique_id)}-{slugify(routine)}",
)
if entity_id:
entity_registry.async_remove(entity_id)
@@ -2,9 +2,10 @@
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import slugify
from .const import DOMAIN
from .coordinator import AmazonDevicesCoordinator
@@ -50,3 +51,32 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
and self._serial_num in self.coordinator.data
and self.device.online
)
class AmazonServiceEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Defines Alexa Devices entity for service device."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AmazonDevicesCoordinator,
description: EntityDescription,
) -> None:
"""Initialize the service entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, service_device_id(coordinator))},
manufacturer="Amazon",
entry_type=DeviceEntryType.SERVICE,
)
self.entity_description = description
self._attr_unique_id = (
f"{slugify(coordinator.config_entry.unique_id)}-{description.key}"
)
def service_device_id(coordinator: AmazonDevicesCoordinator) -> str:
"""Return service device id."""
return slugify(f"{coordinator.config_entry.unique_id}_service_device")
@@ -7,7 +7,7 @@ from pyatv.interface import AppleTV, KeyboardListener
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.const import CONF_NAME
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -23,23 +23,33 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Load Apple TV binary sensor based on a config entry."""
# apple_tv config entries always have a unique id
manager = config_entry.runtime_data
cb: CALLBACK_TYPE
added = False
@callback
def setup_entities(atv: AppleTV) -> None:
nonlocal added
if added:
return
if atv.features.in_state(FeatureState.Available, FeatureName.TextFocusState):
assert config_entry.unique_id is not None
name: str = config_entry.data[CONF_NAME]
async_add_entities(
[AppleTVKeyboardFocused(name, config_entry.unique_id, manager)]
)
cb()
added = True
cb = async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
config_entry.async_on_unload(
async_dispatcher_connect(
hass, f"{SIGNAL_CONNECTED}_{config_entry.unique_id}", setup_entities
)
)
config_entry.async_on_unload(cb)
# The manager may have already connected (and dispatched SIGNAL_CONNECTED)
# before this platform was forwarded, in which case the signal above was
# missed; handle that case directly.
if manager.atv is not None:
setup_entities(manager.atv)
class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener):
@@ -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
),
}
@@ -7,11 +7,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -72,19 +72,6 @@
"id": "Answer ID",
"sentences": "Sentences"
}
},
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
options:
- first
- last
- any
translation_key: trigger_behavior
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
@@ -194,6 +194,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"switch",
"temperature",
"text",
"timer",
"todo",
"update",
"vacuum",
@@ -900,8 +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()
self.action_script.async_unload()
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
await self._async_disable(stop_actions=False)
await self.action_script.async_unload()
if self._condition is not None:
self._condition.async_unload()
+7 -1
View File
@@ -18,4 +18,10 @@ DEFAULT_STREAM_PROFILE = "No stream profile"
DEFAULT_TRIGGER_TIME = 0
DEFAULT_VIDEO_SOURCE = "No video source"
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CAMERA, Platform.LIGHT, Platform.SWITCH]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.CAMERA,
Platform.EVENT,
Platform.LIGHT,
Platform.SWITCH,
]
+62
View File
@@ -0,0 +1,62 @@
"""Support for Axis event entities."""
from __future__ import annotations
from dataclasses import dataclass
from axis.models.event import Event, EventTopic
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity
DOORBELL_CONFIG = ("I8116-E", "0")
@dataclass(frozen=True, kw_only=True)
class AxisEventPlatformDescription(AxisEventDescription, EventEntityDescription):
"""Axis event entity description."""
ENTITY_DESCRIPTIONS = (
AxisEventPlatformDescription(
key="Doorbell",
device_class=EventDeviceClass.DOORBELL,
event_types=[DoorbellEventType.RING],
event_topic=EventTopic.PORT_INPUT,
name_fn=lambda _hub, _event: "Doorbell",
supported_fn=lambda hub, event: (hub.config.model, event.id) == DOORBELL_CONFIG,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AxisConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up an Axis event platform."""
config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisEvent, ENTITY_DESCRIPTIONS
)
class AxisEvent(AxisEventEntity, EventEntity):
"""Representation of an Axis event entity."""
entity_description: AxisEventPlatformDescription
@callback
def async_event_callback(self, event: Event) -> None:
"""Handle Axis event updates."""
if event.is_tripped:
self._trigger_event(DoorbellEventType.RING)
self.async_write_ha_state()
+1 -1
View File
@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==69"],
"requirements": ["axis==71"],
"ssdp": [
{
"manufacturer": "AXIS"
+15 -5
View File
@@ -30,19 +30,29 @@ BATTERY_PERCENTAGE_DOMAIN_SPECS = {
CONDITIONS: dict[str, type[Condition]] = {
"is_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_ON, support_duration=True
BATTERY_DOMAIN_SPECS,
STATE_ON,
primary_entities_only=False,
),
"is_not_low": make_entity_state_condition(
BATTERY_DOMAIN_SPECS, STATE_OFF, support_duration=True
BATTERY_DOMAIN_SPECS,
STATE_OFF,
primary_entities_only=False,
),
"is_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_ON, support_duration=True
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_ON,
primary_entities_only=False,
),
"is_not_charging": make_entity_state_condition(
BATTERY_CHARGING_DOMAIN_SPECS, STATE_OFF, support_duration=True
BATTERY_CHARGING_DOMAIN_SPECS,
STATE_OFF,
primary_entities_only=False,
),
"is_level": make_entity_numerical_condition(
BATTERY_PERCENTAGE_DOMAIN_SPECS, PERCENTAGE
BATTERY_PERCENTAGE_DOMAIN_SPECS,
PERCENTAGE,
primary_entities_only=False,
),
}
@@ -3,16 +3,14 @@
entity:
- domain: binary_sensor
device_class: battery
primary_entities_only: false
fields:
behavior: &condition_behavior
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for: &condition_for
required: true
default: 00:00:00
@@ -42,6 +40,7 @@ is_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -51,6 +50,7 @@ is_not_charging:
entity:
- domain: binary_sensor
device_class: battery_charging
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
@@ -60,8 +60,10 @@ is_level:
entity:
- domain: sensor
device_class: battery
primary_entities_only: false
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
+3 -15
View File
@@ -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%]"
}
@@ -69,21 +72,6 @@
"name": "Battery is not low"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Battery",
"triggers": {
"level_changed": {
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
@@ -33,11 +33,13 @@ from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
from homeassistant.config_entries import (
SOURCE_USER,
ConfigEntry,
ConfigFlowResult,
ConfigSubentry,
ConfigSubentryData,
ConfigSubentryFlow,
FlowType,
SubentryFlowContext,
SubentryFlowResult,
)
from homeassistant.const import (
@@ -62,7 +64,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
from .binary_sensor import above_greater_than_below, no_overlapping
from .const import (
CONF_OBSERVATIONS,
CONF_P_GIVEN_F,
CONF_P_GIVEN_T,
CONF_PRIOR,
@@ -373,26 +374,6 @@ def _validate_observation_subentry(
return user_input
async def _validate_subentry_from_config_entry(
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
) -> dict[str, Any]:
# Standard behavior is to merge the result with the options.
# In this case, we want to add a subentry so we update the options directly.
observations: list[dict[str, Any]] = handler.options.setdefault(
CONF_OBSERVATIONS, []
)
if handler.parent_handler.cur_step is not None:
user_input[CONF_PLATFORM] = handler.parent_handler.cur_step["step_id"]
user_input = _validate_observation_subentry(
user_input[CONF_PLATFORM],
user_input,
other_subentries=handler.options[CONF_OBSERVATIONS],
)
observations.append(user_input)
return {}
async def _get_description_placeholders(
handler: SchemaCommonFlowHandler,
) -> dict[str, str]:
@@ -420,48 +401,12 @@ async def _get_description_placeholders(
}
async def _get_observation_menu_options(handler: SchemaCommonFlowHandler) -> list[str]:
"""Return the menu options for the observation selector."""
options = [typ.value for typ in ObservationTypes]
if handler.options.get(CONF_OBSERVATIONS):
options.append("finish")
return options
CONFIG_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
str(USER): SchemaFlowFormStep(
CONFIG_SCHEMA,
validate_user_input=_validate_user,
next_step=str(OBSERVATION_SELECTOR),
description_placeholders=_get_description_placeholders,
),
str(OBSERVATION_SELECTOR): SchemaFlowMenuStep(
_get_observation_menu_options,
),
str(ObservationTypes.STATE): SchemaFlowFormStep(
STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
# Prevent the name of the bayesian sensor from being used as the suggested
# name of the observations
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.NUMERIC_STATE): SchemaFlowFormStep(
NUMERIC_STATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
str(ObservationTypes.TEMPLATE): SchemaFlowFormStep(
TEMPLATE_SUBSCHEMA,
next_step=str(OBSERVATION_SELECTOR),
validate_user_input=_validate_subentry_from_config_entry,
suggested_values=None,
description_placeholders=_get_description_placeholders,
),
"finish": SchemaFlowFormStep(),
)
}
@@ -497,27 +442,17 @@ class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
name: str = options[CONF_NAME]
return name
@callback
def async_create_entry(
self,
data: Mapping[str, Any],
**kwargs: Any,
) -> ConfigFlowResult:
"""Finish config flow and create a config entry."""
data = dict(data)
observations = data.pop(CONF_OBSERVATIONS)
subentries: list[ConfigSubentryData] = [
ConfigSubentryData(
data=observation,
title=observation[CONF_NAME],
subentry_type="observation",
unique_id=None,
)
for observation in observations
]
self.async_config_flow_finished(data)
return super().async_create_entry(data=data, subentries=subentries, **kwargs)
async def async_on_create_entry(self, result: ConfigFlowResult) -> ConfigFlowResult:
"""Start subentry flow when config entry has been created."""
subentry_result = await self.hass.config_entries.subentries.async_init(
(result["result"].entry_id, "observation"),
context=SubentryFlowContext(source=SOURCE_USER),
)
result["next_flow"] = (
FlowType.CONFIG_SUBENTRIES_FLOW,
subentry_result["flow_id"],
)
return result
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
+4
View File
@@ -13,3 +13,7 @@ UNKNOWN = "unknown"
DEFAULT_HOST = "192.168.0.2"
DEFAULT_PORT = 80
LIGHT_MAX_KELVINS = 6500 # 154 Mireds
LIGHT_MIN_KELVINS = 2700 # 370 Mireds
+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:
+46 -9
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
import math
from typing import Any
import blebox_uniapi.light
@@ -22,9 +23,9 @@ from homeassistant.components.light import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import color as color_util
from . import BleBoxConfigEntry
from .const import LIGHT_MAX_KELVINS, LIGHT_MIN_KELVINS
from .entity import BleBoxEntity
_LOGGER = logging.getLogger(__name__)
@@ -59,8 +60,8 @@ COLOR_MODE_MAP = {
class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Representation of BleBox lights."""
_attr_min_color_temp_kelvin = 2700 # 370 Mireds
_attr_max_color_temp_kelvin = 6500 # 154 Mireds
_attr_min_color_temp_kelvin = LIGHT_MIN_KELVINS
_attr_max_color_temp_kelvin = LIGHT_MAX_KELVINS
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
@@ -78,10 +79,43 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
"""Return the name."""
return self._feature.brightness
def _color_temp_to_native_scale(self, x: int) -> int:
"""Convert color temperature from Kelvin to native BleBox scale (0-255).
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = (
(self._attr_max_color_temp_kelvin - x)
/ (self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin)
) * 255
# note: within the operating temperature range here the Kelvin
# scale has less "integer steps" than the native scale used
# by blebox devices. Thus we need to use rounding method that is opposite
# to the one used in _color_temp_from_native_scale in order to avoid
# temperature value jumping by one step when the temperature value is read
# back from the device
bounded = max(min(math.floor(scaled), 255), 0)
return int(bounded)
def _color_temp_from_native_scale(self, x: int) -> int:
"""Convert color temperature from native BleBox scale (0-255) to Kelvin.
BleBox native scale is inverted relative to Kelvin: 0=warm (2700K), 255=cold (6500K).
"""
scaled = self._attr_max_color_temp_kelvin - (x / 255) * (
self._attr_max_color_temp_kelvin - self._attr_min_color_temp_kelvin
)
# note: see _color_temp_to_native_scale for explanation of rounding method
bounded = max(
min(math.ceil(scaled), self._attr_max_color_temp_kelvin),
self._attr_min_color_temp_kelvin,
)
return int(bounded)
@property
def color_temp_kelvin(self) -> int:
"""Return the color temperature value in Kelvin."""
return color_util.color_temperature_mired_to_kelvin(self._feature.color_temp)
return self._color_temp_from_native_scale(self._feature.color_temp)
@property
def color_mode(self) -> ColorMode:
@@ -139,15 +173,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
effect = kwargs.get(ATTR_EFFECT)
color_temp_kelvin = kwargs.get(ATTR_COLOR_TEMP_KELVIN)
rgbww = kwargs.get(ATTR_RGBWW_COLOR)
rgb = kwargs.get(ATTR_RGB_COLOR)
feature = self._feature
value = feature.sensible_on_value
rgb = kwargs.get(ATTR_RGB_COLOR)
if rgbw is not None:
value = list(rgbw)
if color_temp_kelvin is not None:
value = feature.return_color_temp_with_brightness(
int(color_util.color_temperature_kelvin_to_mired(color_temp_kelvin)),
self._color_temp_to_native_scale(color_temp_kelvin),
self.brightness,
)
@@ -162,14 +197,16 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
if brightness is not None:
if self.color_mode == ColorMode.COLOR_TEMP:
value = feature.return_color_temp_with_brightness(
color_util.color_temperature_kelvin_to_mired(
self.color_temp_kelvin
),
self._color_temp_to_native_scale(self.color_temp_kelvin),
brightness,
)
else:
value = feature.apply_brightness(value, brightness)
if isinstance(value, (list, tuple)) and not any(value):
await self._feature.async_off()
return
try:
await self._feature.async_on(value)
except ValueError as exc:
@@ -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.3"],
"zeroconf": ["_bbxsrv._tcp.local."]
}
@@ -58,6 +58,7 @@ from .api import (
async_address_present,
async_ble_device_from_address,
async_clear_address_from_match_history,
async_clear_advertisement_history,
async_current_scanners,
async_discovered_service_info,
async_get_advertisement_callback,
@@ -116,6 +117,7 @@ __all__ = [
"async_address_present",
"async_ble_device_from_address",
"async_clear_address_from_match_history",
"async_clear_advertisement_history",
"async_current_scanners",
"async_discovered_service_info",
"async_get_advertisement_callback",
+13
View File
@@ -207,6 +207,19 @@ def async_clear_address_from_match_history(hass: HomeAssistant, address: str) ->
_get_manager(hass).async_clear_address_from_match_history(address)
@hass_callback
def async_clear_advertisement_history(hass: HomeAssistant, address: str) -> None:
"""Clear cached advertisement history for a device.
Causes the next advertisement from this address to be treated as new
data, bypassing the change-detection guard in the Bluetooth manager.
Intended for devices that emit static advertisements as a wake-up
signal, for example, devices that require an active GATT connection
to read sensor data and whose advertisement payload never changes.
"""
_get_manager(hass).async_clear_advertisement_history(address)
@hass_callback
def async_register_scanner(
hass: HomeAssistant,
+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}"
}
+1 -2
View File
@@ -293,9 +293,8 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="uptime",
translation_key="last_restart",
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda data: data.uptime,
),
@@ -151,9 +151,6 @@
"laser_remaining_life": {
"name": "Laser remaining lifetime"
},
"last_restart": {
"name": "Last restart"
},
"magenta_drum_page_counter": {
"name": "Magenta drum page counter",
"unit_of_measurement": "[%key:component::brother::entity::sensor::page_counter::unit_of_measurement%]"
+38 -4
View File
@@ -38,7 +38,14 @@ from homeassistant.helpers.device_registry import (
)
from homeassistant.helpers.typing import ConfigType
from .const import CONF_HEATING_CIRCUITS, CONF_PASSKEY, DEFAULT_PORT, DOMAIN, LOGGER
from .const import (
CONF_HEATING_CIRCUITS,
CONF_PASSKEY,
DEFAULT_HEATING_CIRCUITS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
@@ -118,7 +125,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
# Read available heating circuits from config entry data
# (populated by config flow or migration)
circuits: list[int] = entry.data[CONF_HEATING_CIRCUITS]
circuits: list[int] = entry.data[CONF_HEATING_CIRCUITS] or list(
DEFAULT_HEATING_CIRCUITS
)
# Fetch required device metadata in parallel for faster startup
device, info = await asyncio.gather(
@@ -229,7 +238,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
# heating circuits from the device; fall back to [1] (pre-multi-circuit
# default) if the device is unreachable or the endpoint is unsupported.
if entry.version == 1 and entry.minor_version < 2:
circuits: list[int] = [1]
circuits: list[int] = list(DEFAULT_HEATING_CIRCUITS)
config = BSBLANConfig(
host=entry.data[CONF_HOST],
passkey=entry.data[CONF_PASSKEY],
@@ -245,11 +254,18 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
except (BSBLANError, TimeoutError) as err:
LOGGER.warning(
"Circuit discovery during migration failed for %s (%s); "
"defaulting to single circuit [1]. Use Reconfigure to "
"defaulting to a single circuit. Use Reconfigure to "
"rediscover additional circuits later",
entry.data[CONF_HOST],
err,
)
if not circuits:
LOGGER.warning(
"Circuit discovery during migration returned no heating circuits "
"for %s; defaulting to a single circuit",
entry.data[CONF_HOST],
)
circuits = list(DEFAULT_HEATING_CIRCUITS)
hass.config_entries.async_update_entry(
entry,
@@ -263,4 +279,22 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) ->
circuits,
)
# 1.2 -> 1.3: Repair entries that stored an empty circuit list during
# discovery. Every BSB-LAN setup has at least one heating circuit.
if entry.version == 1 and entry.minor_version < 3:
if not entry.data[CONF_HEATING_CIRCUITS]:
LOGGER.warning(
"Stored heating circuits for %s are empty; defaulting to a "
"single circuit",
entry.data[CONF_HOST],
)
data = {
**entry.data,
CONF_HEATING_CIRCUITS: list(DEFAULT_HEATING_CIRCUITS),
}
else:
data = {**entry.data}
hass.config_entries.async_update_entry(entry, data=data, minor_version=3)
return True
+18 -4
View File
@@ -15,21 +15,28 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from .const import CONF_HEATING_CIRCUITS, CONF_PASSKEY, DEFAULT_PORT, DOMAIN, LOGGER
from .const import (
CONF_HEATING_CIRCUITS,
CONF_PASSKEY,
DEFAULT_HEATING_CIRCUITS,
DEFAULT_PORT,
DOMAIN,
LOGGER,
)
class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a BSBLAN config flow."""
VERSION = 1
MINOR_VERSION = 2
MINOR_VERSION = 3
def __init__(self) -> None:
"""Initialize BSBLan flow."""
self.host: str = ""
self.port: int = DEFAULT_PORT
self.mac: str | None = None
self.circuits: list[int] = [1]
self.circuits: list[int] = list(DEFAULT_HEATING_CIRCUITS)
self.passkey: str | None = None
self.username: str | None = None
self.password: str | None = None
@@ -386,6 +393,13 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
try:
await bsblan.initialize()
self.circuits = await bsblan.get_available_circuits()
if not self.circuits:
LOGGER.debug(
"Circuit discovery returned no heating circuits for %s, "
"defaulting to single circuit",
self.host,
)
self.circuits = list(DEFAULT_HEATING_CIRCUITS)
except (
BSBLANError,
TimeoutError,
@@ -394,4 +408,4 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
"Circuit discovery not available for %s, defaulting to single circuit",
self.host,
)
self.circuits = [1]
self.circuits = list(DEFAULT_HEATING_CIRCUITS)
+1
View File
@@ -24,4 +24,5 @@ ATTR_OUTSIDE_TEMPERATURE: Final = "outside_temperature"
CONF_PASSKEY: Final = "passkey"
CONF_HEATING_CIRCUITS: Final = "heating_circuits"
DEFAULT_HEATING_CIRCUITS: Final = (1,)
DEFAULT_PORT: Final = 80
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.2.0"],
"requirements": ["python-bsblan==5.2.1"],
"zeroconf": [
{
"name": "bsb-lan*",
@@ -81,6 +81,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)
)
@@ -176,6 +182,12 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
end=self.to_local(self.get_end_date(vevent)),
location=get_attr_value(vevent, "location"),
description=get_attr_value(vevent, "description"),
uid=get_attr_value(vevent, "uid"),
recurrence_id=(
str(v)
if (v := get_attr_value(vevent, "recurrence_id")) is not None
else None
),
)
@staticmethod
@@ -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),
}
@@ -7,11 +7,8 @@ is_event_active:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -64,12 +64,6 @@
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_offset_type": {
"options": {
"after": "After",
+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,
}
@@ -7,11 +7,13 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for: &condition_for
required: true
default: 00:00:00
selector:
duration:
.humidity_threshold_entity: &humidity_threshold_entity
- domain: input_number
@@ -39,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
@@ -58,6 +51,7 @@ is_hvac_mode:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
hvac_mode:
context:
filter_target: target
@@ -73,6 +67,7 @@ target_humidity:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
@@ -85,6 +80,7 @@ target_temperature:
target: *condition_climate_target
fields:
behavior: *condition_behavior
for: *condition_for
threshold:
required: true
selector:
+21 -15
View File
@@ -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%]"
}
@@ -271,21 +292,6 @@
"message": "Provided temperature {check_temp} is not valid. Accepted range is {min_temp} to {max_temp}."
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"set_fan_mode": {
"description": "Sets the fan mode of a thermostat.",
+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),
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for: &trigger_for
required: true
default: 00:00:00
+53 -2
View File
@@ -3,9 +3,10 @@
from aiocomelit.const import BRIDGE
from homeassistant.const import CONF_HOST, CONF_PIN, CONF_PORT, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import CONF_VEDO_PIN, DEFAULT_PORT
from .const import _LOGGER, CONF_VEDO_PIN, DEFAULT_PORT, DOMAIN
from .coordinator import (
ComelitBaseCoordinator,
ComelitConfigEntry,
@@ -81,6 +82,56 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
return True
async def async_migrate_entry(
hass: HomeAssistant, config_entry: ComelitConfigEntry
) -> bool:
"""Migrate old entry."""
if config_entry.version > 1:
# This means the user has downgraded from a future version
return False
if config_entry.version == 1 and config_entry.minor_version == 1:
device_registry = dr.async_get(hass)
@callback
def update_unique_id(entry: er.RegistryEntry) -> dict[str, str] | None:
if (
entry.domain != Platform.SENSOR
or entry.device_id is None
or not (device_entry := device_registry.async_get(entry.device_id))
or not any(
platform == DOMAIN
and identifier.startswith(f"{config_entry.entry_id}-zone-")
for platform, identifier in device_entry.identifiers
)
):
return None
_LOGGER.debug(
"Migrating from version %s.%s",
config_entry.version,
config_entry.minor_version,
)
zone_index = entry.unique_id.removeprefix(f"{config_entry.entry_id}-")
return {
"new_unique_id": f"{config_entry.entry_id}-human_status-{zone_index}"
}
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
hass.config_entries.async_update_entry(config_entry, version=1, minor_version=2)
_LOGGER.info(
"Migration to version %s.%s successful",
config_entry.version,
config_entry.minor_version,
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> bool:
"""Unload a config entry."""
@@ -94,6 +94,7 @@ class ComelitConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Comelit."""
VERSION = 1
MINOR_VERSION = 2
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -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:
+1 -1
View File
@@ -153,7 +153,7 @@ class ComelitVedoSensorEntity(
super().__init__(coordinator)
# Use config_entry.entry_id as base for unique_id
# because no serial number or mac is available
self._attr_unique_id = f"{config_entry_entry_id}-{zone.index}"
self._attr_unique_id = f"{config_entry_entry_id}-{description.key}-{zone.index}"
self._attr_device_info = coordinator.platform_device_info(zone, "zone")
self.entity_description = description
@@ -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)
+4 -1
View File
@@ -273,7 +273,10 @@ class Control4Climate(Control4Entity, ClimateEntity):
if data is None:
return None
humidity = data.get(CONTROL4_HUMIDITY)
return int(humidity) if humidity is not None else None
try:
return int(humidity) if humidity is not None else None
except ValueError, TypeError:
return None
@property
def hvac_mode(self) -> HVACMode:
@@ -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"]
}
@@ -7,11 +7,13 @@ is_value:
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
selector:
duration:
threshold:
required: true
selector:
+4 -15
View File
@@ -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"
}
@@ -43,21 +47,6 @@
}
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"decrement": {
"description": "Decrements a counter by its step size.",
@@ -7,12 +7,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
+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."""
@@ -3,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -210,21 +210,6 @@
"name": "Window"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"services": {
"close_cover": {
"description": "Closes a cover.",
+2 -6
View File
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
+10 -2
View File
@@ -4,10 +4,11 @@ from datetime import timedelta
import logging
from pydaikin.daikin_base import Appliance
from pydaikin.exceptions import DaikinException
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, TIMEOUT_SEC
@@ -33,4 +34,11 @@ class DaikinCoordinator(DataUpdateCoordinator[None]):
self.device = device
async def _async_update_data(self) -> None:
await self.device.update_status()
try:
await self.device.update_status()
except DaikinException as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="error_communicating",
translation_placeholders={"error": str(err)},
) from err
@@ -59,6 +59,9 @@
}
},
"exceptions": {
"error_communicating": {
"message": "Error communicating with Daikin device: {error}"
},
"zone_hvac_mode_unsupported": {
"message": "Zone temperature can only be changed when the main climate mode is heat or cool."
},
@@ -7,6 +7,7 @@ from typing import Any
from devolo_plc_api import Device
from devolo_plc_api.exceptions.device import DeviceNotFound
from yarl import URL
from homeassistant.components import zeroconf
from homeassistant.const import (
@@ -17,6 +18,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.httpx_client import get_async_client
from .const import (
@@ -123,6 +125,25 @@ async def async_setup_entry(
entry.runtime_data.coordinators = coordinators
# Ensure the device exists before forwarding to platforms, so that the
# device tracker (which looks up the device by wifi station MAC) is not
# racing the other platforms that create the device via DeviceInfo.
device_info = dr.DeviceInfo(
configuration_url=URL.build(scheme="http", host=device.ip),
identifiers={(DOMAIN, str(device.serial_number))},
manufacturer="devolo",
model=device.product,
model_id=device.mt_number,
serial_number=device.serial_number,
sw_version=device.firmware_version,
)
if device.mac:
device_info["connections"] = {(dr.CONNECTION_NETWORK_MAC, device.mac)}
dr.async_get(hass).async_get_or_create(
config_entry_id=entry.entry_id,
**device_info,
)
await hass.config_entries.async_forward_entry_setups(entry, platforms(device))
entry.async_on_unload(
@@ -117,7 +117,7 @@ SENSOR_TYPES: dict[str, DevoloSensorEntityDescription[Any, Any]] = {
key=LAST_RESTART,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_class=SensorDeviceClass.TIMESTAMP,
device_class=SensorDeviceClass.UPTIME,
value_func=_last_restart,
),
}
@@ -75,9 +75,6 @@
"connected_wifi_clients": {
"name": "Connected Wi-Fi clients"
},
"last_restart": {
"name": "Last restart of the device"
},
"neighboring_wifi_networks": {
"name": "Neighboring Wi-Fi networks"
},
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["aiodns==4.0.0"]
"requirements": ["aiodns==4.0.3"]
}
@@ -3,11 +3,8 @@
required: true
default: any
selector:
select:
translation_key: condition_behavior
options:
- all
- any
automation_behavior:
mode: condition
for:
required: true
default: 00:00:00
@@ -31,21 +31,6 @@
"name": "Door is open"
}
},
"selector": {
"condition_behavior": {
"options": {
"all": "All",
"any": "Any"
}
},
"trigger_behavior": {
"options": {
"any": "Any",
"first": "First",
"last": "Last"
}
}
},
"title": "Door",
"triggers": {
"closed": {
+2 -6
View File
@@ -3,12 +3,8 @@
required: true
default: any
selector:
select:
translation_key: trigger_behavior
options:
- first
- last
- any
automation_behavior:
mode: trigger
for:
required: true
default: 00:00:00
+14 -4
View File
@@ -1,24 +1,34 @@
"""The Duco integration."""
from __future__ import annotations
import re
from duco import DucoClient, build_ssl_context
from duco_connectivity import DucoClient
from homeassistant.const import CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
from .coordinator import DucoConfigEntry, DucoCoordinator
_REMOVED_SENSOR_RE = re.compile(r"_\d+_(box_)?temperature$")
async def async_setup_entry(hass: HomeAssistant, entry: DucoConfigEntry) -> bool:
"""Set up Duco from a config entry."""
ssl_context = await hass.async_add_executor_job(build_ssl_context)
# Remove entity registry entries for the temperature and box_temperature
# sensors that were removed when migrating to python-duco-connectivity.
entity_registry = er.async_get(hass)
for entity_entry in er.async_entries_for_config_entry(
entity_registry, entry.entry_id
):
if _REMOVED_SENSOR_RE.search(entity_entry.unique_id):
entity_registry.async_remove(entity_entry.entity_id)
client = DucoClient(
session=async_get_clientsession(hass),
host=entry.data[CONF_HOST],
ssl_context=ssl_context,
)
coordinator = DucoCoordinator(hass, entry, client)
+2 -4
View File
@@ -5,8 +5,8 @@ from __future__ import annotations
import logging
from typing import Any
from duco import DucoClient, build_ssl_context
from duco.exceptions import DucoConnectionError, DucoError
from duco_connectivity import DucoClient
from duco_connectivity.exceptions import DucoConnectionError, DucoError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -160,11 +160,9 @@ class DucoConfigFlow(ConfigFlow, domain=DOMAIN):
Returns a tuple of (box_name, mac_address).
"""
ssl_context = await self.hass.async_add_executor_job(build_ssl_context)
client = DucoClient(
session=async_get_clientsession(self.hass),
host=host,
ssl_context=ssl_context,
)
board_info = await client.async_get_board_info()
lan_info = await client.async_get_lan_info()
+1 -1
View File
@@ -6,4 +6,4 @@ from homeassistant.const import Platform
DOMAIN = "duco"
PLATFORMS = [Platform.FAN, Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=30)
SCAN_INTERVAL = timedelta(seconds=10)
+3 -3
View File
@@ -5,9 +5,9 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
from duco import DucoClient
from duco.exceptions import DucoConnectionError, DucoError
from duco.models import BoardInfo, Node
from duco_connectivity import DucoClient
from duco_connectivity.exceptions import DucoConnectionError, DucoError
from duco_connectivity.models import BoardInfo, Node
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
+16 -2
View File
@@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import asdict
from typing import Any
from duco.exceptions import DucoConnectionError
from duco_connectivity.exceptions import DucoConnectionError
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST
@@ -15,6 +15,9 @@ from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .coordinator import DucoConfigEntry
# MAC addresses and serial numbers are redacted because a Duco installer or
# manufacturer could cross-reference them against an installation registry to
# identify the physical location of the device.
TO_REDACT = {
CONF_HOST,
"mac",
@@ -33,22 +36,33 @@ async def async_get_config_entry_diagnostics(
coordinator = entry.runtime_data
board = asdict(coordinator.board_info)
# `time` is a Unix epoch timestamp of the last board info fetch; not useful for support triage.
board.pop("time")
if board["public_api_version"] is None:
board.pop("public_api_version")
if board["software_version"] is None:
board.pop("software_version")
try:
api_info_obj = await coordinator.client.async_get_api_info()
lan_info = await coordinator.client.async_get_lan_info()
duco_diags = await coordinator.client.async_get_diagnostics()
write_remaining = await coordinator.client.async_get_write_req_remaining()
write_remaining = await coordinator.client.async_get_write_requests_remaining()
except DucoConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
) from err
api_info: dict[str, Any] = {"public_api_version": api_info_obj.public_api_version}
if api_info_obj.reported_api_version is not None:
api_info["reported_api_version"] = api_info_obj.reported_api_version
return async_redact_data(
{
"entry_data": entry.data,
"board_info": board,
"api_info": api_info,
"lan_info": asdict(lan_info),
"nodes": {
str(node_id): asdict(node)
+1 -3
View File
@@ -1,8 +1,6 @@
"""Base entity for the Duco integration."""
from __future__ import annotations
from duco.models import Node
from duco_connectivity.models import Node
from homeassistant.const import ATTR_VIA_DEVICE
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
import logging
from duco.exceptions import DucoError, DucoRateLimitError
from duco.models import Node, NodeType, VentilationState
from duco_connectivity.exceptions import DucoError, DucoRateLimitError
from duco_connectivity.models import Node, NodeType, VentilationState
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.core import HomeAssistant
+6
View File
@@ -7,6 +7,12 @@
"iaq_rh": {
"default": "mdi:water-percent"
},
"target_flow_level": {
"default": "mdi:gauge"
},
"time_state_end": {
"default": "mdi:timer-outline"
},
"ventilation_state": {
"default": "mdi:tune-variant"
}
+2 -2
View File
@@ -11,9 +11,9 @@
"documentation": "https://www.home-assistant.io/integrations/duco",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["duco"],
"loggers": ["duco_connectivity"],
"quality_scale": "platinum",
"requirements": ["python-duco-client==0.3.9"],
"requirements": ["python-duco-connectivity==0.4.0"],
"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][]].*",
+40 -22
View File
@@ -4,9 +4,10 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
import logging
from duco.models import Node, NodeType, VentilationState
from duco_connectivity.models import Node, NodeType, VentilationState
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -19,11 +20,11 @@ from homeassistant.const import (
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import DucoConfigEntry, DucoCoordinator
@@ -38,7 +39,7 @@ PARALLEL_UPDATES = 0
class DucoSensorEntityDescription(SensorEntityDescription):
"""Duco sensor entity description."""
value_fn: Callable[[Node], int | float | str | None]
value_fn: Callable[[Node], datetime | int | float | str | None]
node_types: tuple[NodeType, ...]
@@ -54,29 +55,40 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
key="ventilation_state",
translation_key="ventilation_state",
device_class=SensorDeviceClass.ENUM,
options=[s.lower() for s in VentilationState],
options=[
state.lower()
for state in VentilationState
if state != VentilationState.UNKNOWN
],
value_fn=lambda node: (
node.ventilation.state.lower() if node.ventilation else None
node.ventilation.state.lower()
if node.ventilation and node.ventilation.state != VentilationState.UNKNOWN
else None
),
node_types=(NodeType.BOX,),
),
DucoSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
key="target_flow_level",
translation_key="target_flow_level",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda node: node.sensor.temp if node.sensor else None,
node_types=(NodeType.UCCO2, NodeType.BSRH, NodeType.UCRH),
native_unit_of_measurement=PERCENTAGE,
suggested_display_precision=0,
value_fn=lambda node: (
node.ventilation.flow_lvl_tgt if node.ventilation else None
),
node_types=(NodeType.BOX,),
),
DucoSensorEntityDescription(
key="box_temperature",
translation_key="box_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
value_fn=lambda node: node.sensor.temp if node.sensor else None,
key="time_state_end",
translation_key="time_state_end",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda node: (
dt_util.utc_from_timestamp(node.ventilation.time_state_end).replace(
second=0, microsecond=0
)
if node.ventilation and node.ventilation.time_state_end != 0
else None
),
node_types=(NodeType.BOX,),
),
DucoSensorEntityDescription(
@@ -143,6 +155,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 +179,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
@@ -210,7 +228,7 @@ class DucoSensorEntity(DucoEntity, SensorEntity):
)
@property
def native_value(self) -> int | float | str | None:
def native_value(self) -> datetime | int | float | str | None:
"""Return the sensor value."""
return self.entity_description.value_fn(self._node)
+11 -3
View File
@@ -47,15 +47,18 @@
}
},
"sensor": {
"box_temperature": {
"name": "Box temperature"
},
"iaq_co2": {
"name": "CO2 air quality index"
},
"iaq_rh": {
"name": "Humidity air quality index"
},
"target_flow_level": {
"name": "Target flow level"
},
"time_state_end": {
"name": "Mode end time"
},
"ventilation_state": {
"name": "Ventilation state",
"state": {
@@ -96,5 +99,10 @@
"rate_limit_exceeded": {
"message": "The Duco device has reached its daily write limit. Try again tomorrow."
}
},
"system_health": {
"info": {
"write_requests_remaining": "Remaining write requests today"
}
}
}
@@ -0,0 +1,47 @@
"""Provide info to system health."""
from typing import Any
from duco_connectivity.exceptions import DucoConnectionError
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
from .const import DOMAIN
from .coordinator import DucoConfigEntry
@callback
def async_register(
hass: HomeAssistant, register: system_health.SystemHealthRegistration
) -> None:
"""Register system health callbacks."""
register.async_register_info(system_health_info)
async def _async_get_write_requests_remaining(
config_entry: DucoConfigEntry,
) -> int | dict[str, str]:
"""Get the remaining write-request quota for system health."""
try:
return (
await config_entry.runtime_data.client.async_get_write_requests_remaining()
)
except DucoConnectionError:
return {"type": "failed", "error": "unreachable"}
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page."""
config_entries: list[DucoConfigEntry] = hass.config_entries.async_loaded_entries(
DOMAIN
)
if not config_entries:
return {}
return {
"write_requests_remaining": _async_get_write_requests_remaining(
config_entries[0]
)
}

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