Compare commits

...

892 Commits

Author SHA1 Message Date
Erik 20bafe99ae Remove overwriting of haffmpeg in smartthings test 2026-01-16 09:38:00 +01:00
Erwin Douna fc281b2fae Firefly III fix background task (#160935) 2026-01-14 21:24:44 +01:00
Abílio Costa 3b111287d5 Remove entity performance optimization section from copilot-instructions (#160944) 2026-01-14 19:36:52 +00:00
Marc Mueller 00f42efc7e Update PyNaCl to 1.6.2 (#160909) 2026-01-14 18:21:09 +01:00
Erik Montnemery 9b9f94414b Add shared helper to assert conditions are hidden behind labs flag (#160941) 2026-01-14 16:53:17 +00:00
Erik Montnemery f01653633d Add shared enable_experimental_triggers_conditions test fixture (#160937) 2026-01-14 16:01:06 +00:00
Erik Montnemery 1ace3e248f Add create_target_condition test helper (#160936) 2026-01-14 16:19:41 +01:00
epenet d9bde85b58 Mark device_class type hints as compulsory in binary_sensor platform (#160934) 2026-01-14 16:18:04 +01:00
Joost Lekkerkerker 766a50abd7 Translate Hikvision NVR channel device name (#160862) 2026-01-14 16:16:26 +01:00
Niracler 9e6073099c Add button platform to sunricher_dali (#160908) 2026-01-14 16:02:25 +01:00
Erik Montnemery 892618d2ff Add fan conditions (#160832)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2026-01-14 15:50:22 +01:00
epenet 79c4164e03 Mark device_class type hints as compulsory in various platforms (#160929) 2026-01-14 15:47:17 +01:00
epenet 77dd4189b1 Mark device_class type hints as compulsory in sensor platform (#160931) 2026-01-14 15:46:40 +01:00
karwosts 4dbab23ada Duration selector for timer.change (#160645) 2026-01-14 15:45:32 +01:00
Erik Montnemery ce7f1a6f6a Adjust docstring in entity registry (#160926) 2026-01-14 15:14:46 +01:00
Erik Montnemery 6fc28298aa Update matter test snapshots (#160924) 2026-01-14 14:53:02 +01:00
Artur Pragacz 0130919128 Improve entity id generation (#160302) 2026-01-14 14:34:52 +01:00
Erik Montnemery 200627a695 Simplify light condition tests (#160910) 2026-01-14 14:15:51 +01:00
epenet 82926f8e9d Mark send_message type hints as compulsory in notify (#160850) 2026-01-14 13:01:33 +01:00
Arie Catsman 07fc81361b Bump pyenphase from 2.4.2 to 2.4.3 (#160912) 2026-01-14 12:57:05 +01:00
Martin Hjelmare bd8aed8e63 Bump zwave-js-server-python to 0.68.0 (#160911) 2026-01-14 12:55:48 +01:00
Martin Hjelmare 2c1693d50a Fix Generate requirements task (#160916) 2026-01-14 12:54:15 +01:00
Marek Tyburec 6e60b70691 Add SmartThings media-player audio notifications (#153287) 2026-01-14 12:50:27 +01:00
Erik Montnemery ac889feb75 Minor optimization of light conditions (#160915) 2026-01-14 11:49:56 +00:00
Erik Montnemery a902f3bb00 Improve comments in trigger and condition test helpers (#160830) 2026-01-14 11:42:32 +00:00
Erwin Douna fcb0c9500b Firefly III expand asyncio.gather usage (#160913) 2026-01-14 12:19:02 +01:00
Abílio Costa f049fbdf77 Add calendar event_started/event_ended triggers (#159659) 2026-01-14 11:12:17 +00:00
dependabot[bot] 20102cd83f Bump j178/prek-action from 1.0.11 to 1.0.12 (#160902)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-14 10:28:11 +01:00
Erik Montnemery 6d6324dae5 Fix some reversed asserts in sensor group tests (#160905) 2026-01-14 09:43:26 +01:00
Erik Montnemery 2ee5410a6c Remove set of _attr_extra_state_attributes in sensor group (#160846)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-01-14 09:21:54 +01:00
Erik Montnemery 56f02a41ca Adjust sensor group behavior (#152167) 2026-01-14 08:23:34 +01:00
Erwin Douna d43102de1b Bump pyportainer 1.0.23 (#160878) 2026-01-14 07:09:35 +01:00
Ludovic BOUÉ 2bcd02b296 Add MatterOutdoorTemperature attribute to Matter binary sensor discovery schema only if OutdoorTemperature exists (#160879) 2026-01-14 06:58:55 +01:00
Brett Adams ad11c72488 Add retry logic to Teslemetry coordinators (#160756)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 01:36:43 +01:00
Manu ddfa6f83c3 Refactor Namecheap DNS update logic to use a coordinator (#160863) 2026-01-14 01:34:27 +01:00
epenet 85baf7a41d Improve type hints in mobile_app notify (#160853)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2026-01-14 01:26:10 +01:00
epenet bf4d5a0bab Improve type hints in telegram notify (#160855) 2026-01-14 01:26:00 +01:00
Erwin Douna 16527ba707 Melcloud small config flow refactor (#160892) 2026-01-14 01:15:36 +01:00
Brett Adams 0612ea4ee8 Bump tesla-fleet-api to 1.4.2 (#159616)
Co-authored-by: Claude <noreply@anthropic.com>
2026-01-14 01:14:58 +01:00
Ville Skyttä 9e842152f7 Upgrade prettier-plugin-sort to 4.2.0 (#160894) 2026-01-14 01:13:16 +01:00
Erwin Douna 63e79c3639 Firefly III add asyncio.gather pattern (#160886) 2026-01-14 01:12:44 +01:00
Erwin Douna d0e4a7fa75 Melcloud Pythonic refactor init (#160891) 2026-01-14 00:38:41 +01:00
Glenn de Haan 815976b9a4 Add HDFury sensor platform (#160628) 2026-01-14 00:35:48 +01:00
scheric 86a5cc5edb Add keep_alive to generic_thermostat config flow (#156641)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-01-13 23:20:40 +00:00
Björn Ebbinghaus 3ebc08c5ec Prefer explicit DeviceClass over hint in entity_id in homekit (#152507)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2026-01-13 23:00:58 +00:00
Paul Bottein 1bcbebb00c Use config entity category for Matter door lock operating mode (#160507) 2026-01-13 23:46:54 +01:00
Jan Bouwhuis 2895225552 Improve test coverage on mobile app legacy notify service action (#160869) 2026-01-13 22:39:01 +01:00
Erwin Douna f4f772ea31 Bump pyfirefly 0.1.11 (#160877) 2026-01-13 22:37:32 +01:00
Manu 66f60e6757 Add reconfigure flow to Namecheap integration (#160870) 2026-01-13 19:47:50 +00:00
Lukas 72d299f088 Mark pooldose as strictly typed (#160779)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-13 19:40:52 +00:00
Thomas55555 9c66561381 Make pollutants dynamic in Google Air Quality (#160747) 2026-01-13 19:28:41 +00:00
Erik Montnemery e762f839fa Improve sensor group tests (#160854) 2026-01-13 20:16:06 +01:00
Joost Lekkerkerker 0c9d97c89f Unmark integrations with a config flow as legacy (#160861) 2026-01-13 19:59:39 +01:00
Robert Resch fb3ee34c81 Bump prek to 0.2.28 (#160864) 2026-01-13 18:59:07 +01:00
Daniel Hjelseth Høyer cb99400128 Add Tibber binary sensors (#160365)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-13 18:56:14 +01:00
divers33 58ef925a07 Refactor MELCloud integration to use DataUpdateCoordinator (#160131)
Co-authored-by: divers33 <divers33@users.noreply.github.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-13 18:52:37 +01:00
Paul Tarjan 41bbfb8725 Add camera platform support to Hikvision integration (#160252)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-13 18:38:18 +01:00
Manu ed226e31b1 Remove defusedxml dependency from Namecheap DynamicDNS integration (#160656) 2026-01-13 18:16:50 +01:00
Robert Resch e900bb9770 Add support for packaging version >= 26 on the version bump script (#160858) 2026-01-13 18:14:46 +01:00
Matthias Alphart d173d25072 Refactor KNX expose entity class (#160705) 2026-01-13 17:25:46 +01:00
Colin 0959896984 openevse: Use a data update coordinator (#160757)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-13 17:04:56 +01:00
epenet 4a3ae454b8 Improve type hints in pushsafer notify (#160851) 2026-01-13 16:46:01 +01:00
Joost Lekkerkerker f2cf6b69bf Use extended entity descriptions in openevse (#160611) 2026-01-13 16:44:29 +01:00
epenet 176f847ebb Split Tuya climate wrappers (#160839) 2026-01-13 16:38:40 +01:00
epenet 277419aafb Fix logging in mycroft notify (#160852) 2026-01-13 16:28:17 +01:00
Willem-Jan van Rootselaar d2b8d165d7 Optimize BSB-Lan integration startup (#160784) 2026-01-13 16:07:33 +01:00
Jamin bf74e67700 Bump voip-utils to 0.3.5 (#160848) 2026-01-13 16:03:55 +01:00
Chris 5c3b85a37a Add authentication to config flow in openevse (#160521)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-13 16:03:40 +01:00
Manu 8543f3f989 Add config flow to Namecheap DynamicDNS integration (#160841) 2026-01-13 15:46:15 +01:00
Sebastian YEPES 52a8a66a91 Bump qingping-ble to 1.1.0 (#160815) 2026-01-13 15:35:50 +01:00
dependabot[bot] 002a931e70 Bump github/codeql-action from 4.31.9 to 4.31.10 (#160829)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-13 15:33:27 +01:00
Daniel Hjelseth Høyer 0667bfc81d Remove old migration for Tibber (#160845) 2026-01-13 15:31:28 +01:00
Michael Hansen 329b2c840d Revert back to microVAD (#160821) 2026-01-13 08:09:17 -06:00
Robert Resch ea7e94bcc1 Replace pre-commit by prek (#160427) 2026-01-13 15:09:02 +01:00
nasWebio cc30add73a Add climate platform to NASweb integration (#141583)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-01-13 14:55:12 +01:00
Simone Chemelli 21cfb9a0e5 Add guest Wi-Fi QR code for Vodafone Station (#160307)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-13 13:57:59 +01:00
Erik Montnemery 143eadd887 Remove progress_step date entry flow decorator (#160844) 2026-01-13 13:52:57 +01:00
Erik Montnemery 855da1d070 Adjust light condition test (#160831) 2026-01-13 10:58:34 +01:00
AlCalzone d5be76d7e6 Make integration scaffolding a bit more newbie-friendly (#160837) 2026-01-13 10:39:49 +01:00
Matthias Alphart 5f396332df Update xknx to 3.14.0 (#160813) 2026-01-13 10:22:49 +01:00
Kevin Stillhammer 56e638e170 accept leading zeros in sms_code for fressnapf_tracker (#160834) 2026-01-13 10:18:15 +01:00
Norbert Rittel 52b90c7706 Make light conditions consistent with triggers and actions (#160477) 2026-01-13 09:45:31 +01:00
Erik Montnemery a6221d16b6 Add helper for creating entity condition tests (#160425) 2026-01-13 08:25:41 +01:00
tronikos 51701cab7c Bump opower to 0.16.2 (#160822) 2026-01-12 19:20:06 -08:00
Raphael Hehl 010e1f2d0d Bump uiprotect to 8.1.1 (#160816) 2026-01-12 23:06:50 +01:00
Jonathan de Jong 66909fc9ca Support HVAC mode in set temperature calls in Mill (#155416)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-01-12 21:46:20 +01:00
Lukas 90a28c95c8 Bump python-pooldose to 0.8.2 (#160800) 2026-01-12 20:20:33 +01:00
Erik Montnemery 83f2c53e8c Disable pyright type checking in VS Code (#160528) 2026-01-12 20:19:19 +01:00
Ludovic BOUÉ 514b6e243c Rename Matter Eve Thermostat Fixture to eve_thermo_v4 (#160796) 2026-01-12 20:16:13 +01:00
Krisjanis Lejejs 742230c7be Bump hass-nabucasa from 1.8.0 to 1.9.0 (#160788) 2026-01-12 19:50:48 +01:00
Ludovic BOUÉ acb6b1444e Add fixture for Matter Eve Thermo 20ECD1701 (v5) with detailed attributes (#160795) 2026-01-12 18:52:18 +01:00
Erwin Douna f358b2231a Add match case in perform action (#160150) 2026-01-12 18:25:51 +01:00
Joakim Sørensen fd24cffa6b Block untill done while setting up cloud in tests (#160780) 2026-01-12 17:32:06 +01:00
Yuxin Wang 0b5d6ee538 Add TIMESTAMP device classes to corresponding sensors in APCUPSD (#160577) 2026-01-12 17:10:25 +01:00
DeerMaximum d125bb88d1 Use load_json_object_fixture in tests for NINA (#160690)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-12 17:09:18 +01:00
Ludovic BOUÉ 2ab51f582a Add Matter occupied setback for thermostats (#155439) 2026-01-12 16:47:43 +01:00
epenet f9b32811b2 Move typed ConfigEntry to coordinator module in point (#160786) 2026-01-12 16:34:38 +01:00
seppwabala 41a423e140 Add support for eds0065 in onewire (#160094)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-12 16:21:00 +01:00
Xiangxuan Qu f717867657 Pass config_entry explicitly to Point coordinator (#160578) 2026-01-12 15:55:41 +01:00
J. Nick Koston ab202a03db Handle deleted issue during repair flow translation check (#160698) 2026-01-12 15:52:36 +01:00
Álvaro Fernández Rojas 46a3e5e5b5 Fix Airzone Q-Adapt select entities (#160695)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2026-01-12 15:48:07 +01:00
Krisjanis Lejejs 0163a4d289 Bump hass-nabucasa from 1.7.0 to 1.8.0 (#160775) 2026-01-12 15:46:49 +01:00
Willem-Jan van Rootselaar 6c1bf31a3c Bump python-bsblan to version 4.1.0 (#160676) 2026-01-12 15:44:03 +01:00
Michael a434760a80 Complete entity name and icon translations in FRITZ!Box Tools (#160746)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-12 15:43:28 +01:00
Jevgeni Kiski 798990fadc Bump vallox-websocket-api to 6.0.0 (#160742) 2026-01-12 15:30:17 +01:00
Glenn de Haan b3d9d92e4a Add HDFury diagnostics (#160641) 2026-01-12 15:08:19 +01:00
Lukas 1082a9ca69 Pooldose: Sync with docs update (#160190) 2026-01-12 14:41:46 +01:00
Joost Lekkerkerker c247f56658 Fix fitbit icon (#160750) 2026-01-12 11:08:59 +01:00
Paul Tarjan e7f71781f1 Fix Hikvision NVR binary sensors not being detected (#160254)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:04:30 +01:00
Josef Zweck c4b2c5e621 Fix missing key for brew by weight in lamarzocco (#160722) 2026-01-12 11:03:36 +01:00
Thomas55555 7779609a76 Add more pollutants to Google Air Quality (#160738) 2026-01-12 11:02:18 +01:00
Duco Sebel 7b9a5f897c Bump python-homewizard-energy to 10.0.1 (#160736) 2026-01-12 10:59:55 +01:00
epenet 6eccbfc1cf Fix Requirement parsing in RequirementsManager (#160485) 2026-01-12 10:55:39 +01:00
Artur Pragacz 0da518e951 Fix scrape sensor device name (#160765) 2026-01-12 10:53:25 +01:00
Bram Kragten e5851b7920 Update frontend to 20260107.1 (#160644) 2026-01-12 10:51:49 +01:00
Artur Pragacz 1b9364e8b5 Assign device_entry earlier in entity platform (#160767) 2026-01-12 10:49:01 +01:00
Carter Green 8460d4f5e2 Yolink diagnostic sensors (#160749) 2026-01-12 10:33:49 +01:00
Artur Pragacz 8fd35cd70d Rename registry imports in entity platform (#160766) 2026-01-12 10:27:03 +01:00
MarkGodwin 88be115699 Bump tplink_omada quality scale to bronze (#160762) 2026-01-12 09:52:46 +01:00
J. Nick Koston 7f4063f91e Bump aiodns to 4.0.0 (#160707) 2026-01-11 07:31:31 -10:00
mattreim 080ba46885 Add model id RODRET wireless dimmer (#160636) 2026-01-11 18:22:19 +01:00
Brett Adams 2cb028ee79 Catch any migration failures in Teslemetry (#160549) 2026-01-11 16:46:30 +01:00
mettolen 72655dbf0b Pump pysaunum to 0.2.0 (#160668) 2026-01-11 16:14:45 +01:00
Erwin Douna 153278221d Bump pytado 0.18.16 (#160724) 2026-01-11 13:24:22 +01:00
Daniel Hjelseth Høyer 4942ce7e86 Better handling of ratelimiting from Tibber (#160599)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-11 11:40:27 +01:00
hanwg 98e918cd8a Improve polling error messages for Telegram bot (#160675) 2026-01-11 06:54:50 +01:00
J. Nick Koston 1efc87bfef Bump easyenergy to 2.2.0 (#160709) 2026-01-10 18:54:50 -10:00
Simon Delberghe b4360ccbd9 Move condition to prioritize preset mode (eco/comfort...) instead of program name in Overkiz (#160189) 2026-01-10 23:58:19 +01:00
Ernst Klamer ce234d69a7 Revert bthome-ble back to 3.16.0 to fix missing data (#160694) 2026-01-10 09:47:30 -10:00
Álvaro Fernández Rojas b2a198e230 Update aioairzone to v1.0.5 (#160688) 2026-01-10 20:43:10 +01:00
Michael Hansen 538009d2df Bump pysilero-vad to 3.2.0 (#160691) 2026-01-10 13:35:46 -06:00
Clifford Roche 99329851a2 Bump greeclimate to 2.1.1 (#160683) 2026-01-10 19:51:04 +01:00
DeerMaximum f8ec395e96 Use snapshots for binary sensor tests in Nina (#160532) 2026-01-10 17:47:29 +01:00
mettolen 98fe189edf Add recalibrate CO2 button to Airobot (#160679) 2026-01-10 17:37:14 +01:00
Samuel Xiao 7b413e3fd3 Bumb switchbot api to v2.10.0 (#160657) 2026-01-10 13:01:55 +01:00
Paul Tarjan 00ca5473d4 Bump pyhik to 0.4.0 (#160654) 2026-01-10 08:04:29 +01:00
Martin Hjelmare 33c808713e Fix Z-Wave creating notification binary sensor for idle state (#160604) 2026-01-10 02:43:13 +01:00
Sid c97437fbf3 Add the professionel5e filter series to eheimdigital (#155550) 2026-01-09 21:24:01 +01:00
Jordan Harvey ad8f14fec1 Bump pynintendoparental to 2.3.2 (#160626)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-01-09 20:09:31 +01:00
karwosts 7df586eff1 Use duration selector for timer service (#160391) 2026-01-09 20:07:32 +01:00
Manu f6fa95d2f7 Rename Namecheap FreeDNS to Dynamic DNS (#160625) 2026-01-09 19:37:03 +01:00
Tero Paloheimo 23a8300012 Add Ruuvi IAQS to Ruuvi BLE (#160529) 2026-01-09 19:04:30 +01:00
Glenn de Haan 694d67d2d5 Add HDFury switch platform (#160620) 2026-01-09 18:08:37 +01:00
mettolen a26c910db7 Add number entities to Saunum integration (#160444) 2026-01-09 18:04:49 +01:00
mettolen ac9d04624b Update Airobot integration to gold quality tier (#160525) 2026-01-09 18:02:27 +01:00
James a0ec7bde33 Introduce better types in Yardian coordinator (#152641)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-09 17:55:08 +01:00
Vasily G. 5f7dc49215 Spotify: user Liked Songs collection playable (#160452)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-09 17:48:39 +01:00
LG-ThinQ-Integration f79eef150e Add humidifier entity for humidifier and dehumidifier to LG ThinQ (#152593)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2026-01-09 17:41:20 +01:00
Arie Catsman 1733599442 Change device class to energy_storage for some enphase_envoy battery entities (#160603) 2026-01-09 16:48:00 +01:00
Thomas55555 3bde4f606b Bump google-air-quality-api to 2.1.2 (#160561) 2026-01-09 16:40:38 +01:00
Christopher Fenner afb635125c Bump PyViCare to 2.55.1 (#156875) 2026-01-09 16:39:31 +01:00
James 876d54ad4d Yardian: Add sensors (#153020)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-09 16:31:29 +01:00
Tom Matheussen c20cd8fb94 Add missing segment speed icons for WLED (#160597) 2026-01-09 15:42:23 +01:00
Colin e15b2ec0cb openevse: Add device_info and unique_id to sensors (#160543)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-09 15:02:07 +01:00
azerty9971 1829452ef1 Change Tuya covers to prefer set_position instead of instruction_wrapper (#160526) 2026-01-09 14:31:31 +01:00
Dan Čermák 9d8dc9ec06 Fix JSON serialization of time objects in anthropic tool results (#160459)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-01-09 12:06:36 +01:00
Bram Kragten 72a3523193 Fix trigger selectors (#160519) 2026-01-09 11:43:33 +01:00
Maciej Bieniek 7c3541e983 Fix AttributeError for missing/incomplete health data in Tractive (#160553) 2026-01-09 10:55:33 +01:00
Michael 8246fc78fa Fix for older Fritzbox models which do not support smarthome triggers (#160555) 2026-01-09 10:52:44 +01:00
tronikos 78dd3aee10 Bump opower to 0.16.1 (#160588) 2026-01-09 10:51:39 +01:00
Brett Adams c22e578aca Fix config flow bug in Tesla Fleet (#160591) 2026-01-09 10:41:33 +01:00
Brett Adams 1021c1959e Fix Climate signal in Teslemetry (#160571) 2026-01-09 10:41:18 +01:00
Brett Adams d3161d8e92 Fix translation of unknown response in Teslemetry & Tesla Fleet (#160506) 2026-01-09 10:16:00 +01:00
Johann Kellerman fc468b56c8 Bump pysma to 1.1.0 (#160583) 2026-01-09 10:14:15 +01:00
Markus Jacobsen ea48dc3c58 Add battery charging binary sensor to Bang & Olufsen (#160527)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-01-09 09:59:20 +01:00
cdnninja 11dde08d79 Correct vesync missing return type (#160580) 2026-01-09 08:09:31 +01:00
epenet 5e43708a40 Skip Tuya update if it is not relevent (#160407) 2026-01-09 07:01:43 +01:00
osohotwateriot 1ac2280266 Change nettleie to grid fee in english strings (#160516) 2026-01-08 23:11:42 +00:00
puddly 6b1ad8d2d1 Bump serialx to v0.6.2 (#160545) 2026-01-08 23:10:29 +00:00
Michael Hansen c1741237f4 Bump pysilero-vad to 3.1.0 (#160554) 2026-01-08 23:09:18 +00:00
LG-ThinQ-Integration 8ecacd6490 Add target_humidity_step attribute to humidifier (#156906)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2026-01-08 23:06:31 +00:00
Glenn de Haan 188ab3930c Add HDFury button platform (#160548) 2026-01-08 22:14:23 +01:00
Michael Hansen a8dba53185 Revert "Update voluptuous and voluptuous-openapi" (#160530) 2026-01-08 10:25:46 -06:00
Erwin Douna a2ef0c9a75 Portainer add prune unused images (#160137)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-08 17:05:45 +01:00
Jan Bouwhuis 5a1fe17580 Bump Intergas Incomfort-client to v0.6.11 (#160520) 2026-01-08 16:44:21 +01:00
ElCruncharino 34388f52a6 Add asyncio-level timeout to Backblaze B2 uploads (#160468) 2026-01-08 16:39:47 +01:00
DeerMaximum fc2199fcf7 Add bronze quality scale for NINA (#155191) 2026-01-08 15:53:43 +01:00
DeerMaximum 2236f8cd07 Fix typo in NINA config flow (#160523) 2026-01-08 15:44:50 +01:00
Klaas Schoute 8d376027bf Add support for gas meter in Powerfox integration (#158196)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-08 14:53:00 +01:00
JHSL 47e91bc2ec Add dishwasher program Dishcare.Dishwasher.Program.IntensiveFixedZone (#160463) 2026-01-08 14:45:44 +01:00
Zoltán Farkasdi 33d1cdd0ac Refactor netatmo binary sensors (#160352) 2026-01-08 13:24:05 +01:00
Brett Adams f46de054ba Add missing data_description translations to Tessie (#160511)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 13:02:36 +01:00
Brett Adams 741aa714dd Add missing PARALLEL_UPDATES to Tesla Fleet (#160510) 2026-01-08 12:40:38 +01:00
osohotwateriot 5fac7d4ffb Add Nettleie optimization option (#160494) 2026-01-08 12:24:00 +01:00
Glenn de Haan 341c441e61 Add HDFury integration (#159996) 2026-01-08 12:21:04 +01:00
wollew a1edf0a77c fix rain sensor for some rare velux windows (#160504) 2026-01-08 12:19:40 +01:00
Erik Montnemery dd84b52c7b Bump python-otbr-api to 2.7.1 (#160496) 2026-01-08 12:10:39 +01:00
Etienne C. 43ced677e5 Get the polling state of a sensor from a template (#159900) 2026-01-08 12:03:45 +01:00
Ville Skyttä 7a696935ed Add icons for Nord Pool highest and lowest price sensors (#159729) 2026-01-08 11:27:17 +01:00
Deyan Petrov be3be360a7 Make Tuya binary sensor consider only updated properties (#160404)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2026-01-08 09:47:27 +01:00
Mick Vleeshouwer 092ebaaeb1 Bump pyOverkiz to 1.19.4 (#160457) 2026-01-08 08:41:30 +01:00
Retha Runolfsson e8025317ed Bump PySwitchbot to 0.76.0 (#160470) 2026-01-08 08:39:23 +01:00
wollew 39b025dfea catch and wrap exceptions when doing pyvlx actions in velux entities (#160430)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-08 00:06:26 +01:00
DeerMaximum 1b436a8808 Use async_configure in NINA to set flow data in tests (#160435)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-07 23:48:42 +01:00
Markus Jacobsen a7440e3756 Add battery support to Bang & Olufsen (#159994) 2026-01-07 23:40:22 +01:00
wollew 2c7852f94b remove workaround for recognition of closed velux windows (#160433) 2026-01-07 23:39:37 +01:00
Maikel Punie bd4653f830 Update velbus quality scale rules for docs (#160200) 2026-01-07 23:32:45 +01:00
Tero Paloheimo c0b2847a87 Update ruuvitag-ble to 0.4.0 (#160441) 2026-01-07 23:32:03 +01:00
J. Diego Rodríguez Royo 8853f6698b Add steam mode and hot air gentle programs to Home Connect (#160445) 2026-01-07 23:10:20 +01:00
Artem Draft b1a3ad6ac3 Improve Bravia TV logging messages (#160394) 2026-01-07 23:09:46 +01:00
Arie Catsman dafa2e69e2 Optimize enphase_envoy code for on_phase use (#160448) 2026-01-07 23:09:00 +01:00
Chris 2c6d6f8ab4 Add unique_id to openevse user flow and import flow (#160436)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 23:06:25 +01:00
J. Diego Rodríguez Royo 10d32b7f23 Bump aiohomeconnect to version 0.28.0 (#160438) 2026-01-07 20:44:36 +01:00
TheJulianJES e4dc4e0ced Bump ZHA to 0.0.84 (#160440) 2026-01-07 19:57:09 +01:00
Maikel Punie 6f9794f235 Add icon translations for velbus (#160439) 2026-01-07 19:26:47 +01:00
Paul Bottein b8cff13737 Fix hvac_mode validation in climate.hvac_mode_changed trigger (#160364) 2026-01-07 17:44:03 +01:00
Bram Kragten 7777714cc0 Update frontend to 20260107.0 (#160434) 2026-01-07 17:34:23 +01:00
Chris f15d5cdf2a Add zeroconf discovery to openevse (#160318)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 16:42:32 +01:00
DeerMaximum 6181f4e7de NINA Use MockConfigEntry to setup integration in test (#160324) 2026-01-07 16:33:06 +01:00
Robert Resch 80df3b5b80 Bump deebot-client to 17.0.1 (#160428) 2026-01-07 16:07:11 +01:00
Simone Chemelli 6e32a2aa18 Bump aiovodafone to 3.1.1 (#160429) 2026-01-07 15:34:46 +01:00
Abílio Costa 3b575fe3e3 Support target triggers in automation relation extraction (#160369) 2026-01-07 15:15:44 +01:00
Joost Lekkerkerker 229400de98 Make Watts depend on the cloud integration (#160424) 2026-01-07 15:07:24 +01:00
Norbert Rittel e963adfdf0 Fix capitalization in openevse data_description string (#160423) 2026-01-07 14:53:19 +01:00
Simone Chemelli fd7bbc68c6 Bump aioshelly to 13.23.1 (#160420) 2026-01-07 14:49:18 +01:00
Robert Resch 9281ab018c Constraint aiomqtt>=2.5.0 to fix blocking call (#160410)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-07 14:21:49 +01:00
Andres Ruiz 80baf86e23 Add codeowners and integration_type for waterfurnace (#160397) 2026-01-07 13:12:58 +01:00
Simone Chemelli db497b23fe Small cleanup for Vodafone Station tests (#160415) 2026-01-07 12:50:12 +01:00
cdnninja a2fb8f5a72 Add Vesync Air Fryer Sensors (#160170)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-07 12:41:34 +01:00
hanwg 6953bd4599 Fix schema validation error in Telegram (#160367) 2026-01-07 12:27:17 +01:00
Xiangxuan Qu 225be65f71 Fix IndexError in Israel Rail sensor when no departures available (#160351)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 12:22:39 +01:00
momala454 7b0463f763 Add additional lens modes 4 to 10 to JVC projector remote (#159657)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-07 12:22:19 +01:00
Luke Lashley 4d305b657a Bump python-roborock to 4.2.1 (#160398) 2026-01-07 11:23:40 +01:00
Paul Tarjan d5a553c8c7 Fix Ring integration log flooding for accounts without subscription (#158012)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-01-07 11:14:05 +01:00
Ivan Dlugos 9169b68254 Bump sentry-sdk to 2.48.0 (#159415)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 11:05:38 +01:00
Colin fde9bd95d5 Replace openevse backend library (#160325) 2026-01-07 10:25:15 +01:00
Marc Mueller e4db8ff86e Update guppy3 to 3.1.6 (#160356) 2026-01-07 10:11:01 +01:00
Erik Montnemery a084e51345 Add test helpers for numerical state triggers (#160308)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-01-07 08:53:35 +01:00
Luke Lashley 00381e6dfd Remove q7 total cleaning time for Roborock (#160399) 2026-01-06 20:27:09 -08:00
Michael Hansen b6d493696a Bump intents to 2026.1.6 (#160389) 2026-01-06 17:11:54 -06:00
Artem Draft 5f0500c3cd Add SSL support in Bravia TV (#160373)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2026-01-06 23:59:47 +01:00
dontinelli c61a63cc6f Bump solarlog_cli to 0.7.0 (#160382) 2026-01-06 23:59:16 +01:00
Raphael Hehl 5445a4f40f Bump uiprotect to 8.0.0 (#160384)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-01-06 23:57:19 +01:00
Daniel Hjelseth Høyer 2888cacc3f Bump pyTibber to 0.34.1 (#160380)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-01-06 23:56:26 +01:00
TheJulianJES 16f3e6d2c9 Bump ZHA to 0.0.83 (#160342) 2026-01-06 12:11:40 -05:00
Bram Kragten 7a872970fa Update frontend to 20251229.1 (#160372) 2026-01-06 17:53:56 +01:00
Bram Kragten 4f5ca986ce Fix number or entity choose schema (#160358) 2026-01-06 17:23:24 +01:00
Artem Draft b58e058da5 Bump pybravia to 0.4.1 (#160368) 2026-01-06 16:42:58 +01:00
epenet badebe0c7f Refactor Tuya event platform to use DeviceWrapper (#160366) 2026-01-06 16:09:13 +01:00
mettolen 7817ec1a52 Update Saunum integration to gold quality tier (#159783) 2026-01-06 16:07:28 +01:00
epenet c773998946 Remove default in Tuya DeviceWrapper options (#160303) 2026-01-06 13:06:53 +01:00
Mika 2bc9397103 Fix missing state class to solaredge (#160336) 2026-01-06 12:36:49 +01:00
Daniel Hjelseth Høyer 685534b17c Add more Tibber sensors (#160354) 2026-01-06 11:18:35 +01:00
tronikos c740f44bfa Bump opower to 0.16.0 (#160348) 2026-01-06 10:25:43 +01:00
mettolen ce471d0222 Add button entity to Airobot integration (#160169) 2026-01-06 08:57:24 +01:00
Manu 53ed344fe0 Add action exceptions to Duck DNS (#160331) 2026-01-06 08:38:22 +01:00
Manu 5f8f3c961a Remove stale devices in Xbox integration (#160337) 2026-01-06 08:27:42 +01:00
Aidan Timson 9d0c5530f2 Bump systembridgeconnector to 5.3.1 (#160326) 2026-01-06 08:09:25 +01:00
abelyliu d114fe4fbd Add report_type to Tuya diagnostic (#160311) 2026-01-06 07:37:48 +01:00
Daniel Hjelseth Høyer f03d44d5b5 Bump pyTibber to 0.34.0 (#160333) 2026-01-06 00:55:28 +01:00
Frédéric 35f4464d4a Add Resideo X2S Smart Thermostat to Matter fan-only mode list (#160260) 2026-01-06 00:24:20 +01:00
DeerMaximum fc2530e979 Use a fixture in NINA to mock async_setup_entry (#160323) 2026-01-05 21:51:10 +01:00
Manu 354fafda1a Refactor Xbox coordinators (#160174) 2026-01-05 21:31:57 +01:00
Sid 5b0dab479d Bump eheimdigital to 1.5.0 (#160312) 2026-01-05 21:30:05 +01:00
Daniel Hjelseth Høyer 1e1f414849 Fix unit for Tibber sensor (#160319) 2026-01-05 21:27:00 +01:00
Xidorn Quan 7c81df6c5c Fix rain count sensors' state class of Ecowitt (#158204) 2026-01-05 21:02:21 +01:00
J. Nick Koston 95d7c42e6a Require service_uuid and service_data_uuid to match hue ble (#160321) 2026-01-05 19:47:31 +01:00
Joakim Sørensen 19fd80035e Add connection check before registering cloudhook URL (#160284) 2026-01-05 09:35:49 -05:00
Martin Hjelmare 8e30787ae6 Test hassfest translations gen_strings_schema (#159464) 2026-01-05 15:27:03 +01:00
epenet 7133da928f Add max_value/min_value/value_step to Tuya DeviceWrapper (#160300) 2026-01-05 14:56:17 +01:00
Colin 3f9a41d393 Add openevse config flow (#158968)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-05 14:49:36 +01:00
epenet f4caf36204 Use generic DeviceWrapper in Tuya cover (#160301) 2026-01-05 14:45:13 +01:00
Bram Kragten 079866e384 Fix humidifier trigger turned on icon (#160297) 2026-01-05 13:45:31 +01:00
epenet dce0db78aa Use generic DeviceWrapper in Tuya sensor (#160299) 2026-01-05 13:45:08 +01:00
epenet fffc18d28b Use generic DeviceWrapper in more Tuya platforms (#160298) 2026-01-05 13:44:45 +01:00
cdnninja c7cbcbc32d Refactor entity unavailable handling in VeSync (#160274) 2026-01-05 13:17:35 +01:00
Samuel Xiao aebcdd6e7a Switchbot Cloud: Add new supported light (#160282) 2026-01-05 13:12:02 +01:00
Paul Tarjan 3be92510f8 Fix Tesla update showing scheduled updates as installing (#158681) 2026-01-05 12:42:58 +01:00
Matrix 4d8448e82a Bump yolink api to 0.6.1 (#160293) 2026-01-05 12:37:26 +01:00
Daniel Hjelseth Høyer 625bc467d4 Move Tibber to OAuth (#156690)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-05 12:36:41 +01:00
Steven Looman 47573b7f6a Bump async-upnp-client to 0.46.2 (#160188) 2026-01-05 12:33:40 +01:00
epenet 7c95c92525 Make Tuya DeviceWrapper a generic class (#159349) 2026-01-05 12:22:48 +01:00
dotlambda 1aed46e39e Bump google-genai to 1.56.0 (#160210) 2026-01-05 12:21:02 +01:00
epenet 6659166df0 Move Tuya vacuum entity logic to wrapper class (#159255) 2026-01-05 12:20:25 +01:00
Erik Montnemery 1e6b0ba9ec Allow passing trigger options to parametrize_trigger_states (#160119) 2026-01-05 10:44:31 +01:00
abelyliu 1f23098638 Bump tuya-device-sharing-sdk to 0.2.8 (#160288) 2026-01-05 10:28:13 +01:00
epenet 98ee0421b7 Fix Tuya light color data wrapper (#160280) 2026-01-05 10:27:57 +01:00
cdnninja 6aaa57f660 Set PARALLEL_UPDATES in VeSync (#160272) 2026-01-05 09:18:16 +01:00
Andres Ruiz fad817853f Add state_class to waterfurnace sensors (#160277) 2026-01-05 09:17:41 +01:00
Michael 7ef7d3f570 Add entered and left home person triggers (#159320)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2026-01-05 09:09:59 +01:00
Thomas55555 14bca5a052 Make verify_ssl configurable in remote calendar (#160216) 2026-01-04 15:32:38 -08:00
Vincent Courcelle 18769730f0 Bump python-roborock to 4.2.0 (#160184) 2026-01-05 00:26:50 +01:00
Sab44 de6d117d9a Bump librehardwaremonitor-api to version 1.8.4 (#160249) 2026-01-04 21:35:28 +01:00
J. Nick Koston d2deef968a Ensure Brotli >= 1.2.0 (#160229) 2026-01-04 08:08:42 -10:00
Mick Vleeshouwer 6cae1821fb Fix execution history matching to ignore subsystem suffix in diagnostics in Overkiz (#160218) 2026-01-04 11:38:30 +01:00
Jan-Philipp Benecke 8d8046d233 Bump aiowebdav2 to 0.5.0 (#160233) 2026-01-04 11:37:41 +01:00
Samuel Xiao d7a9a980d0 Switchbot Cloud: Fixed Robot Vacuum Cleaner S20 had two device_model name (#160230) 2026-01-04 11:36:24 +01:00
J. Nick Koston ff8ad0c9ba Bump aioesphomeapi to 43.10.1 (#160227) 2026-01-03 17:40:15 -10:00
J. Nick Koston 27728cdca8 Bump aiohttp 3.13.3 (#160206) 2026-01-03 16:46:27 -10:00
Erwin Douna f1eaf78923 Portainer polish ephemeral container ID (#160186) 2026-01-03 21:32:07 +01:00
Willem-Jan van Rootselaar 667b1db594 Bump python-bsblan dependency to version 3.1.6 (#160202) 2026-01-03 21:30:26 +01:00
Josef Zweck d6cad546e1 Remove referral link from fish_audio (#160193) 2026-01-03 17:12:53 +01:00
Tom 4c8ffa2158 Bump airOS to v0.6.1 adding LiteAP AC support (#160194) 2026-01-03 13:52:08 +01:00
Lukas 933fae9ade Pooldose document exempts (#160166) 2026-01-03 08:59:07 +01:00
Erwin Douna b6dd9db76e Portainer add state sensor (#160156) 2026-01-03 08:51:58 +01:00
Manu 11487d6856 Set integration type service in Duck DNS (#160172) 2026-01-03 08:51:18 +01:00
Manu 920e938d84 Add discovery for default hostnames to PlayStation Network (#160173) 2026-01-03 08:44:17 +01:00
Kevin Stillhammer afc256622a raise proper service exceptions in fressnapf_tracker (#159707)
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
2026-01-02 19:53:16 +01:00
Erwin Douna bfef048a7c Bump pyportainer 1.0.22 (#160140) 2026-01-02 18:37:14 +01:00
Maikel Punie bfc8111728 Bump velbus to silver integration scale (#160147) 2026-01-02 18:36:04 +01:00
Maikel Punie ebd6ae7e80 Velbus mark entities unavailable when connection is terminated (#160143) 2026-01-02 17:43:33 +01:00
MarkGodwin dd98a85300 Refactor TP-Link Omada config flow tests (#159950)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-01-02 17:41:04 +01:00
Brett Adams 6568a19ce6 Handle export options when enrolled to VPP in Teslemetry (#157665) 2026-01-02 16:52:03 +01:00
wollew 83c1e8d5b5 bump pyvlx version to 0.2.27 (#160139) 2026-01-02 16:49:09 +01:00
Simone Chemelli c5a06657a3 Remove low level call for Shelly climate (#160065) 2026-01-02 16:47:39 +01:00
Maciej Bieniek 25e54990d2 Bump nextdns to version 5.0.0 (#160138) 2026-01-02 16:33:27 +01:00
Nikoheld 3b2a7ba561 bump nibe to 2.21.0 (#160135) 2026-01-02 16:06:43 +01:00
Åke Strandberg 8f8f896675 Add filling level sensors to miele (#157858) 2026-01-02 15:57:15 +01:00
Willem-Jan van Rootselaar 9539a612a6 Add time synchronization feature to BSB-Lan integration (#156600) 2026-01-02 15:54:37 +01:00
Erwin Douna d6751eb63f Bump pyportainer 1.0.21 (#160130) 2026-01-02 15:06:46 +01:00
Pete Sage b462038126 Use long service timeout for Sonos Unjoin (#160110) 2026-01-02 14:18:56 +01:00
cdnninja ce06446376 Add pm1 and pm10 to vesync (#160072)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2026-01-02 14:15:52 +01:00
Erik Montnemery 8de22e0134 Await writes in shopping_list action handlers (#157420) 2026-01-02 13:41:41 +01:00
mettolen fbd08d4e42 Bump pyairobotrest to 0.2.0 (#160125) 2026-01-02 12:29:29 +01:00
Zoltán Farkasdi 32e0be4535 netatmo: test_camera webhook testing parametrize and light split (#159772) 2026-01-02 11:00:17 +01:00
Maikel Punie 0423639833 Bump velbusaio to 2026.1.1 (#160116) 2026-01-02 09:16:27 +01:00
Jan Bouwhuis 1244d8aa33 Fix reolink brightness scaling (#160106) 2026-01-01 21:56:35 +01:00
Pete Sage 38c37ab33c Improve Sonos wait to unjoin timeout (#160011) 2026-01-01 20:21:25 +01:00
Willem-Jan van Rootselaar 1636eab2e8 Add schema validation for set_hot_water_schedule service (#159990) 2026-01-01 20:16:54 +01:00
Miguel Camba 737a5811a9 Update voluptuous and voluptuous-openapi (#160073) 2026-01-01 20:07:06 +01:00
Austin Mroczek 5f2da20319 Bump total_connect_client to 2025.12.2 (#160075) 2026-01-01 20:02:56 +01:00
Michael Hansen 2aed4fb8e9 Bump intents to 2026.1.1 (#160099) 2026-01-01 19:58:37 +01:00
Lukas 2b10dc4545 Add reconfiguration flow to pooldose (#159978)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-01-01 17:20:33 +01:00
Maikel Punie b5d22a63bb Velbus quality docs updates (#160092) 2026-01-01 17:02:30 +01:00
Maikel Punie e8e19f47cd Velbus Exception translations (#159627)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-01 16:51:39 +01:00
Maikel Punie 97e6643cd7 Bump velbusaio to 2026.1.0 (#160087) 2026-01-01 16:50:28 +01:00
Ben Wolstencroft ee4bb0eef5 Add support for health_overview API endpoint to Tractive integration (#157960)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
2026-01-01 13:06:24 +01:00
Maikel Punie f82bb8f0b8 Use brightness scale in velbus light (#160041) 2026-01-01 13:03:52 +01:00
cdnninja 79b368cfc3 add description to string vesync (#160003) 2025-12-31 22:20:50 +01:00
cdnninja 6da4a006f2 Add Auto Off Switch to VeSync (#160070) 2025-12-31 22:17:33 +01:00
Allen Porter e5f3ccb38d Improve roborock test accuracy/robustness (#160021) 2025-12-31 16:32:53 +01:00
tronikos 560b91b93b Filter out duplicate voices without language code in Google Cloud (#160046) 2025-12-31 16:30:53 +01:00
Pete Sage edd9f50562 bump soco to 0.30.14 for Sonos (#160050) 2025-12-31 16:25:55 +01:00
Paul Tarjan a4b2e84b03 Fix Hikvision thread safety issue when calling async_write_ha_state (#160027) 2025-12-31 15:52:41 +01:00
rlippmann 9da07c2058 remove domain and service slots from Service object (#160039) 2025-12-31 13:34:02 +01:00
Simone Chemelli 8de6785182 Bump aioamazondevices to 11.0.2 (#160016)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-31 12:31:32 +01:00
Anders Melchiorsen 77f6fa8116 Bump eternalegypt to 0.0.18 (#160006) 2025-12-31 10:57:58 +01:00
Anders Melchiorsen 6b6f338e7e Fix netgear_lte unloading (#160008) 2025-12-31 10:53:24 +01:00
David Knowles aa995fb590 Use WATER device_class for Hydrawise sensors (#160018) 2025-12-31 10:47:48 +01:00
Anders Melchiorsen f0fee87b9e Move async_setup_services to async_setup for netgear_lte (#160007) 2025-12-31 10:43:59 +01:00
Erwin Douna 56ab3bf59b Bump pyfirefly 0.1.10 (#160028) 2025-12-31 09:04:40 +01:00
Luke Lashley 24e2720924 Don't prefer cache for Roborock device fetching (#160022) 2025-12-30 13:21:54 -08:00
Erwin Douna bacc2f00af Bump portainer 1.0.19 (#160014) 2025-12-30 21:13:24 +01:00
Manu 6de2d6810b Convert store image URLs to https in Xbox media resolver (#160015) 2025-12-30 21:10:51 +01:00
Allen Porter de07833d92 Update roborock binary sensor tests with snapshots (#159981) 2025-12-30 19:36:32 +01:00
Matthias Alphart b4eff231c3 Update knx-frontend to 2025.12.30.151231 (#159999) 2025-12-30 18:49:02 +01:00
Luke Lashley 98fea46eea Add support for vacuum entity for Roborock Q7 (#159966) 2025-12-30 07:26:18 -08:00
divers33 18e8821891 Add podcast favorites support to Sonos media browser (#159961)
Co-authored-by: divers33 <divers33@users.noreply.github.com>
2025-12-30 15:14:53 +01:00
Sab44 cc2377d44d Bump librehardwaremonitor-api to version 1.7.2 (#159987) 2025-12-30 12:18:50 +01:00
doomsniper09 8370c6abfb Accept integer coordinates in has_location helper (#159835)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-12-30 12:06:23 +01:00
Panda-NZ 2d1a672de5 Add ambient temperature sensor to ToGrill (#159798) 2025-12-30 09:44:23 +01:00
Ernst Klamer 75ea42a834 bump xiaomi-ble to 1.4.1 (#159954) 2025-12-30 00:12:45 +01:00
Lukas 45491e17cd Pooldose Diagnostics (#159965) 2025-12-29 23:03:13 +01:00
Stefan H. b994f03391 Migrate traccar_server to use entry.runtime_data (#156065)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 22:16:01 +01:00
Kamil Breguła 473cb59013 Add translation of exceptions in met (#155765)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 22:12:40 +01:00
J. Nick Koston 9302926d99 Bump aioesphomeapi to 43.9.1 (#159960) 2025-12-29 11:09:37 -10:00
Branden Cash d92516b7c9 Implement reconfigure config flow in SRP energy (#151542)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 21:52:25 +01:00
Luke Lashley 5b561213d3 Bump Python-Roborock to 4.1.0 (#159963) 2025-12-29 21:52:13 +01:00
Erwin Douna 0a16bd4919 Portainer fix stopped container for stats (#159964) 2025-12-29 21:51:46 +01:00
Michael f74a6e2625 Record current Feedreader integration quality scale and set to silver (#143179)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 21:36:23 +01:00
Joost Lekkerkerker ecc271409a Small cleanup in Feedreader (#159962) 2025-12-29 21:31:25 +01:00
Michael 1f63bc3231 Record current Synology DSM integration quality scale (#141245)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 21:24:18 +01:00
Joost Lekkerkerker 78adeb837e Inject session in Switchbot cloud (#159942) 2025-12-29 21:18:34 +01:00
Joost Lekkerkerker bfacf462bf Add integration_type service to nuheat (#159845) 2025-12-29 21:12:23 +01:00
Joost Lekkerkerker 771d40dbf6 Add integration_type hub to permobil (#159872) 2025-12-29 21:12:05 +01:00
Joost Lekkerkerker 8e441242ad Add integration_type hub to pooldose (#159880) 2025-12-29 21:11:46 +01:00
Joost Lekkerkerker b8a4237ab1 Add integration_type hub to poolsense (#159881) 2025-12-29 21:11:17 +01:00
Joost Lekkerkerker e92af1ee76 Add integration_type device to ps4 (#159892) 2025-12-29 21:10:52 +01:00
Matthias Alphart e561c1cebb Fix KNX translation references (#159959) 2025-12-29 20:50:53 +01:00
Franck Nijhof d77f82f8e8 Bump version to 2026.2.0dev0 (#159956) 2025-12-29 20:38:24 +01:00
Joost Lekkerkerker fcc3598d7f Add integration_type device to netgear (#159816) 2025-12-29 21:14:58 +02:00
Joost Lekkerkerker a1a1d65ee4 Add Hood fan speed select entity to SmartThings (#157841)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 19:56:55 +01:00
Louis Christ 8778d4c704 Move actions to async_setup in bluesound (#159809) 2025-12-29 19:44:05 +01:00
Jeremiah Paige 7790a2ebdd Add config flow to wsdot (#149208)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 18:58:09 +01:00
Felipe Santos 585c2dce16 Add OpenRGB profile select entity (#154732) 2025-12-29 17:42:20 +01:00
Tom Matheussen 08d25d388f Address Satel Integra config flow test comments (#159951) 2025-12-29 17:37:01 +01:00
MarkGodwin f06f25b99a Delay creation of some Omada device entities when devices are not connected (#156665) 2025-12-29 17:23:42 +01:00
Joost Lekkerkerker f11791f84d Fix CI by freezing time in Growatt tests (#159946) 2025-12-29 17:18:18 +01:00
Eduardo Tsen 8a3c0edb59 Publish area and floor metrics to Prometheus (#159322) 2025-12-29 17:08:55 +01:00
ElCruncharino e7176c4919 Fix Backblaze B2 timeout issues during backup uploads (#158272) 2025-12-29 17:02:19 +01:00
Willem-Jan van Rootselaar 3327c3513b Add service for setting hot water schedule (#156112)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-29 17:00:50 +01:00
Simone Chemelli 8ca87ef1cb Add support for Comelit Vedo system connected via Comelit Serial bridge (#156301) 2025-12-29 16:59:52 +01:00
Franck Nijhof d90e72c6d4 Update frontend to 20251229.0 (#159945) 2025-12-29 16:59:21 +01:00
Kurt Chrisford 4083bd3c94 Refactor Actron Air climate and switch entities to inherit from a new base entity class (#159540)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 16:57:46 +01:00
wollew fd2a92ffce report unavailable for non-polled velux entities (#159523) 2025-12-29 16:42:17 +01:00
Manu ac2941569e Move actions to module and improve test coverage in Duck DNS (#158079)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 16:41:06 +01:00
Brett Adams 043465e42f Replace access token authentication with OAuth2 in Teslemetry (#158905)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 16:40:45 +01:00
MarkGodwin ec5657753f Move TP-Link Omada update coordinator into coordinator module (#159943) 2025-12-29 16:35:55 +01:00
noambav e2fa95694f Add fish_audio integration (#152000)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 16:34:02 +01:00
Niracler 2e28796ab0 Upgrade sunricher_dali integration to silver quality scale (#159576) 2025-12-29 16:27:10 +01:00
Kamil Breguła bdb456e568 Improve tests in WLED (#157799)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-29 16:26:49 +01:00
Maciej Bieniek e1599dc53a Bump aiotractive to version 0.7.0 (#159939) 2025-12-29 16:24:26 +01:00
jesperraemaekers 0b10c36521 Bump Weheat to 2025.12.24 (#159676) 2025-12-29 16:11:28 +01:00
Tomer f8dd05efde Minor Azure Data Explorer integration fixes (#159677) 2025-12-29 16:06:40 +01:00
Colin Finck 9e0b4c2beb kostal_plenticore: Add DcCheck state (#159679) 2025-12-29 16:06:22 +01:00
Samuel Xiao 315c7db527 Switchbot Cloud: Fixed abnormally high power consumptio (#157156)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 16:06:10 +01:00
Samuel Xiao f3832442be Switchbot Cloud: Bumb switchbot api to v2.9.0 (#159672)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 15:59:46 +01:00
Matrix 97d1c18f21 Add support for YS7914 (#159586) 2025-12-29 15:54:33 +01:00
johanzander 307aea90d6 Increase Growatt Server test coverage to 97% (#159544)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 15:54:14 +01:00
Tom Matheussen 7b8d65b91f Fix Satel Options flow failing (#159736) 2025-12-29 15:52:48 +01:00
DeerMaximum 24f253f775 Add missing default values in NINA config flow (#159708) 2025-12-29 15:52:25 +01:00
dependabot[bot] f2a4d55439 Bump dawidd6/action-download-artifact from 11 to 12 (#159768)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 15:23:53 +01:00
Andrew Jackson 33975f7c7f Add labels to Transmission add_torrent service and events (#159781) 2025-12-29 15:22:00 +01:00
cdnninja 8c9a6ccd6d Add quality scale file to vesync integration (#156663)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 15:10:30 +01:00
Bouwe Westerdijk 6fc3e2dc53 Implement shorter default update_interval for Plugwise P1 (#159626) 2025-12-29 14:42:25 +01:00
J. Diego Rodríguez Royo 28c14f21fa Add new Home Connect washing machine programs (#157174) 2025-12-29 14:42:02 +01:00
Arie Catsman ca912699e3 Fix: Add state_class to enphase_envoy battery entities (#158103) 2025-12-29 14:41:35 +01:00
cdnninja 96d1e3d260 Use runtime_data in VeSync (#159720)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 14:40:35 +01:00
Matthias Alphart 0ea38335d7 Support KNX text entity configuration from UI (#159509) 2025-12-29 14:22:39 +01:00
Franck Nijhof 86be5d9dc3 Merge branch 'master' into dev 2025-12-29 13:21:33 +00:00
MarkGodwin 01d4c42138 Code quality fixes for TP-Link Omada service actions (#159868) 2025-12-29 14:03:43 +01:00
Jordan Harvey a8114b7e4f Add time extended sensor for Nintendo Switch parental controls (#159227) 2025-12-29 14:00:37 +01:00
epenet be7b7f3d25 Revert "Disable blackbird integration (#157817)" (#159369) 2025-12-29 13:59:26 +01:00
Grégoire Seux f9481b6e51 Allow reconfigure open_router subentries (#159503)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 13:55:05 +01:00
surfingbytes 183bc31125 Add Cookidoo planned meals calendar (#159456)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-29 13:54:36 +01:00
Franck Nijhof 46befc257a 2025.12.5 (#159934) 2025-12-29 13:53:33 +01:00
puddly 097d190750 Replace pyserial-asyncio with serialx for ZHA and Hardware integrations (#159375)
Co-authored-by: TheJulianJES <TheJulianJES@users.noreply.github.com>
2025-12-29 13:06:38 +01:00
Josef Zweck 5df03851df CI fix: Exempt caio from license check (#159866) 2025-12-29 11:58:03 +00:00
Franck Nijhof de97a949ac Bump version to 2025.12.5 2025-12-29 11:47:21 +00:00
Víctor Gurbani e9d2f6add2 Add state_class to Nuki battery sensor (#159756) 2025-12-29 11:44:37 +00:00
Allen Porter 5aa0eefd5f Start reauth when roborock notices the MQTT session is unauthorized (#159719) 2025-12-29 11:44:35 +00:00
Allen Porter 7d17f0a00c Fix Roborock repair issue behavior (#159718) 2025-12-29 11:44:34 +00:00
Allen Porter e329eab514 Bump python-roborock to 3.21.1 (#159660) 2025-12-29 11:44:32 +00:00
Allen Porter 3b0ebcaa9e Bump python-roborock to 3.20.1 (#159621) 2025-12-29 11:44:01 +00:00
Robert Svensson 9595cf30bb Bump axis to v66 fixing an issue with latest xmltodict (#159604)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-29 11:41:34 +00:00
Mary efee51548f Fix Ecoforest unknown alarm translation key (#159594) 2025-12-29 11:41:32 +00:00
Raphael Hehl 6db227e4ab Bump uiprotect to 7.33.3 (#159593) 2025-12-29 11:41:00 +00:00
Maikel Punie 4c6074621f Bump valbusaio to 2025.12.0 (#159578) 2025-12-29 11:39:52 +00:00
Magnus 584687f7c4 Bump melissa to 3.0.3 (#159557) 2025-12-29 11:39:51 +00:00
Allen Porter e16335f15b Redact additional unnecessary diagnostic fields (#159546) 2025-12-29 11:39:50 +00:00
Raphael Hehl 960049e7d3 Improve date handling in UniFi Protect media source (#159491)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-29 11:37:22 +00:00
J. Nick Koston 0f61b68324 Bump yalexs-ble to 3.2.4 (#159476) 2025-12-29 11:37:21 +00:00
J. Nick Koston fc5c31b348 Bump yalexs-ble to 3.2.2 (#158124) 2025-12-29 11:37:20 +00:00
Tom Harris cba33133cd Bump insteon panel to 0.6.0 to fix dialog button issues (#159449) 2025-12-29 11:31:46 +00:00
Lukas 9631528c87 Pooldose action exceptions (#159572) 2025-12-29 12:28:54 +01:00
Pete Sage d8b2d026c3 Create issue for Sonos when Sonos system does not have UPnP enabled (#159330) 2025-12-29 11:28:25 +00:00
Åke Strandberg 35ba9c7007 Add openid scope and update OAuth2 url:s in senz integration (#159265) 2025-12-29 11:28:23 +00:00
Rene Nulsch cdb7b9cc25 Fix ZeroDivisionError for inverse unit conversions (#159161) 2025-12-29 11:28:22 +00:00
Paul Tarjan ea9ac1dd36 Change Samsung TV WoL turn_on log from warning to debug (#158676) 2025-12-29 11:28:20 +00:00
Kamil Breguła 8508d48d79 Normalize unique ID in WLED (#157901)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-29 11:28:18 +00:00
dontinelli 2516e80663 Disable quoted cookies for compatibility with older SolarLog devices (#157839) 2025-12-29 11:28:16 +00:00
Artur Pragacz 70096d435a Remove stdlib-list requirement in hassfest docker (#159915) 2025-12-29 12:01:39 +01:00
Erwin Douna 7cfd58dce2 Bump pyportainer 1.0.17 (#159931) 2025-12-29 11:58:42 +01:00
mettolen 2fdfcd6bad Add reconfigure flow to Airobot integration (#159810) 2025-12-29 11:13:35 +01:00
Artur Pragacz 337789cd8c Fix entity id format in smhi (#159662) 2025-12-29 11:02:29 +01:00
Åke Strandberg d87528e068 Add openid scope and update OAuth2 url:s in senz integration (#159265) 2025-12-29 10:53:41 +01:00
Artur Pragacz dc119d47c5 Simplify entity components requirements in hassfest docker (#159914) 2025-12-29 10:50:20 +01:00
Josef Zweck 8aa897b090 Change integration_type of pure_energie to device (#159928) 2025-12-29 10:49:13 +01:00
Jan Bouwhuis 7931cb4773 Add production power sensor that is compatible with the energy power dashboard for supported homewizard devices (#159500) 2025-12-29 10:46:07 +01:00
Daniel Hjelseth Høyer 559d42dc27 Bump Adax-local to 0.3.0 (#159887)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-29 09:59:57 +01:00
J. Nick Koston cfe6cf2448 Bump aioesphomeapi to 43.9.0 (#159924) 2025-12-29 09:58:42 +01:00
Mick Vleeshouwer 85ef06f26c Bump pyOverkiz to 1.19.3 (#159917) 2025-12-29 08:08:20 +01:00
David Recordon 25fc41a934 Explicitly pass config_entry in Control4 integration (#159920) 2025-12-29 08:07:33 +01:00
TheJulianJES 12047e8499 Bump ZHA to 0.0.82 (#159922) 2025-12-29 06:06:28 +01:00
cdnninja 702fd78d86 Fix missing vesync fan string, map modes (#158956) 2025-12-29 00:01:22 +01:00
Amolith 375b0186db Bump voluptuous-openapi to 0.2.0 (#159825)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2025-12-28 23:38:03 +01:00
Matthias Alphart efd6b686a8 Update knx-frontend to 2025.12.28.215221 (#159909) 2025-12-28 23:24:36 +01:00
Kory Prince e68ef21522 ollama integration: Don't drop all falsey values (#159735) 2025-12-28 23:22:34 +01:00
Armin Ghofrani 4f589b144d Fix ElevenLabs STT auto-detect language (#159804) 2025-12-28 23:18:34 +01:00
Martin Böh baa4685df1 Fix Thread dataset update logic when only timestamp ticks change (#159769) 2025-12-28 23:14:32 +01:00
J. Diego Rodríguez Royo 0a6d433594 Bump aiohomeconnect to version 0.26.0 (#159801)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-28 23:12:08 +01:00
Michael ed72e0d4a7 Update list of supported languages in Microsoft TTS (#159830) 2025-12-28 23:08:52 +01:00
Erwin Douna bcdcc1208e Bump pyportainer 1.0.16 (#159904) 2025-12-28 20:20:45 +01:00
Josef Zweck dd53a82fd5 Fix CI: Test triggers test flakyness (#159885) 2025-12-28 19:23:08 +01:00
Josef Zweck b61c6d1edd CI fix: Exempt caio from license check (#159866) 2025-12-28 18:18:29 +01:00
Joost Lekkerkerker ceeec6817e Add integration_type device to prusalink (#159891) 2025-12-28 18:16:15 +01:00
Joost Lekkerkerker 74370bf3ba Add integration_type device to qingping (#159899) 2025-12-28 18:13:24 +01:00
Joost Lekkerkerker 07ef6110ca Add integration_type service to pushover (#159897) 2025-12-28 18:11:31 +01:00
Joost Lekkerkerker 9e68800564 Add integration_type service to pvpc_hourly_pricing (#159898) 2025-12-28 18:11:13 +01:00
Joost Lekkerkerker c8af5bb452 Add integration_type service to pushbullet (#159896) 2025-12-28 18:03:52 +01:00
Joost Lekkerkerker 58069fd473 Add integration_type service to purpleair (#159895) 2025-12-28 18:03:32 +01:00
Joost Lekkerkerker 822227f740 Add integration_type service to pure_energie (#159894) 2025-12-28 18:02:09 +01:00
Joost Lekkerkerker 36eece93ee Add integration_type service to pterodactyl (#159893) 2025-12-28 18:01:45 +01:00
Joost Lekkerkerker 31631bb619 Add integration_type hub to prosegur (#159889) 2025-12-28 16:57:55 +01:00
Joost Lekkerkerker 82e6b52129 Add integration_type device to progettihwsw (#159886) 2025-12-28 16:52:04 +01:00
Joost Lekkerkerker 9298b7787f Add integration_type device to private_ble_device (#159884) 2025-12-28 16:50:35 +01:00
Joost Lekkerkerker f883eeebf3 Add integration_type device to powerwall (#159883) 2025-12-28 16:49:50 +01:00
Joost Lekkerkerker 93fe23081d Add integration_type hub to powerfox (#159882) 2025-12-28 16:48:17 +01:00
Joost Lekkerkerker ca064bf09b Add integration_type hub to point (#159879) 2025-12-28 16:45:27 +01:00
Joost Lekkerkerker 0addd82bf7 Add integration_type service to plex (#159878) 2025-12-28 16:44:29 +01:00
Joost Lekkerkerker 4686968275 Add integration_type hub to plaato (#159877) 2025-12-28 16:44:18 +01:00
Joost Lekkerkerker 7f28f09616 Add integration_type device to p1_monitor (#159869) 2025-12-28 16:42:47 +01:00
Joost Lekkerkerker 1e9af4fbe0 Add integration_type device to panasonic_viera (#159870) 2025-12-28 16:41:56 +01:00
Joost Lekkerkerker 5399655134 Add integration_type service to peco (#159871) 2025-12-28 16:41:25 +01:00
Joost Lekkerkerker cfaba23412 Add integration_type hub to pglab (#159873) 2025-12-28 16:37:40 +01:00
Joost Lekkerkerker c7fa557148 Add integration_type device to pi_hole (#159875) 2025-12-28 16:35:23 +01:00
Joost Lekkerkerker 2b6abb372c Add integration_type service to picnic (#159876) 2025-12-28 16:34:45 +01:00
Joost Lekkerkerker 1ea8023753 Add integration_type device to philips_js (#159874) 2025-12-28 16:12:59 +01:00
Pete Sage 14e79ff311 Add translation string for Sonos unjoin timeout error (#159834) 2025-12-28 15:37:54 +01:00
Joost Lekkerkerker b57e848d5d Add integration_type device to opentherm_gw (#159860) 2025-12-28 15:36:53 +01:00
Joost Lekkerkerker 938d6b6b0d Add integration_type hub to osoenergy (#159863) 2025-12-28 15:35:00 +01:00
Joost Lekkerkerker 31de4a4fa2 Add integration_type service to owntracks (#159865) 2025-12-28 15:31:25 +01:00
Joost Lekkerkerker 88b5b37f07 Add integration_type service to opower (#159862) 2025-12-28 15:30:38 +01:00
Joost Lekkerkerker 17ddba98c1 Add integration_type service to ourgroceries (#159864) 2025-12-28 15:29:59 +01:00
Joost Lekkerkerker 71fd1d079b Add integration_type service to openweathermap (#159861) 2025-12-28 15:26:26 +01:00
Joost Lekkerkerker 08a8836d29 Add integration_type device to nibe_heatpump (#159839) 2025-12-28 15:19:04 +01:00
Joost Lekkerkerker c5261c5bb5 Add integration_type device to netgear_lte (#159817) 2025-12-28 15:16:49 +01:00
Maciej Bieniek a82d00475c Bump accuweather to version 5.0.0 (#159831) 2025-12-28 15:15:01 +01:00
Maciej Bieniek d62251f0a3 Bump gios to version 7.0.0 (#159832) 2025-12-28 15:14:24 +01:00
Joost Lekkerkerker dc88502894 Add integration_type device to obihai (#159851) 2025-12-28 15:13:26 +01:00
Joost Lekkerkerker a8a8017d35 Add integration_type service to nina (#159842) 2025-12-28 15:12:44 +01:00
Joost Lekkerkerker de224b8107 Add integration_type service to nzbget (#159850) 2025-12-28 15:12:32 +01:00
Joost Lekkerkerker 841baa15b6 Add integration_type service to octoprint (#159852) 2025-12-28 15:11:44 +01:00
Joost Lekkerkerker 9a9c968cd2 Add integration_type service to opensky (#159859) 2025-12-28 15:10:59 +01:00
Joost Lekkerkerker f0ddb9ff2c Add integration_type device to openhome (#159858) 2025-12-28 15:08:18 +01:00
Joost Lekkerkerker 8f6d88f517 Add integration_type hub to omnilogic (#159853) 2025-12-28 15:08:04 +01:00
Joost Lekkerkerker 20b0b6beb4 Add integration_type device to opengarage (#159856) 2025-12-28 15:07:25 +01:00
Joost Lekkerkerker 2980187206 Add integration_type service to openexchangerates (#159855) 2025-12-28 15:07:18 +01:00
Joost Lekkerkerker 97998ff61f Add integration_type device to onvif (#159854) 2025-12-28 15:07:13 +01:00
Bartosz Budzyński b1189a33fe Increase ViCare heating max temperature to 60°C (#159847) 2025-12-28 15:05:14 +01:00
Joost Lekkerkerker 90cf2c7592 Add integration_type service to nws (#159849) 2025-12-28 15:04:17 +01:00
Joost Lekkerkerker 9b56229d34 Add integration_type hub to nexia (#159837) 2025-12-28 15:04:04 +01:00
Joost Lekkerkerker 848de08baa Add integration_type service to nextbus (#159838) 2025-12-28 15:03:35 +01:00
Joost Lekkerkerker 7d9bee8cea Add integration_type service to nightscout (#159840) 2025-12-28 15:01:24 +01:00
Joost Lekkerkerker 3a712f6512 Add integration_type hub to niko_home_control (#159841) 2025-12-28 15:01:00 +01:00
Joost Lekkerkerker 40d566f7f7 Add integration_type service to nintendo_parental_controls (#159843) 2025-12-28 15:00:11 +01:00
Joost Lekkerkerker 60ba1b0288 Add integration_type service to nmbs (#159844) 2025-12-28 14:59:49 +01:00
cdnninja c1d77f00b3 vesync switch to async_write_ha_state (#159824) 2025-12-28 09:48:13 +01:00
Josef Zweck 96b2146f2b Fix translations for lamarzocco bbw numbers (#159787) 2025-12-27 18:55:16 +01:00
Andre Lengwenus 83b53e7bc7 Bump pypck to 0.9.9 (#159803) 2025-12-27 17:11:03 +01:00
Matthias Alphart 456a12f612 Update knx-frontend to 2025.12.25.200238 (#159748) 2025-12-27 12:12:17 +01:00
Bouwe Westerdijk a8c732047d Bump plugwise to v1.11.2 (#159780) 2025-12-27 12:09:17 +01:00
Paul Tarjan d9fe37e325 Address reviewer feedback on exception handling in hikvision (#159752) 2025-12-27 11:13:52 +01:00
blob810 c41d14fbe7 Support Shelly wave shutter with firmware 14.2.0 in Z-Wave (#159750)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-27 11:12:02 +01:00
Kevin Stillhammer cf9444dc64 Add reauth to fressnapf_tracker (#157994) 2025-12-26 20:36:17 +01:00
Daniel Rauber a447217b03 kostal_plenticore: Add state_class to Battery SoC sensor (#159776) 2025-12-26 20:31:47 +01:00
Pete Sage e639ebc269 Exceptions during Sonos Unjoin action results in hung script (#159779) 2025-12-26 20:20:12 +01:00
Paul Tarjan 45ba7e0df1 Fix HikCamera.get_event_triggers() call with incorrect argument (#159760) 2025-12-26 11:33:53 +01:00
Manu dfdcdbc856 Add integration type hub to Google Cast (#159757) 2025-12-26 11:26:31 +01:00
Manu ea5df92ab9 Add integration type hub to Xiaomi Home (#159758) 2025-12-26 11:25:38 +01:00
Víctor Gurbani 9d1f500d65 Add state_class to Nuki battery sensor (#159756) 2025-12-26 10:37:16 +01:00
Joost Lekkerkerker a82f500934 Add integration_type hub to moehlenhoff_alpha2 (#159694)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-25 21:56:55 +01:00
Joost Lekkerkerker 71d29ba28e Add integration_type hub to motioneye (#159698) 2025-12-25 21:54:32 +01:00
Joost Lekkerkerker 7910f33140 Add integration_type hub to microbees (#159690) 2025-12-25 21:53:24 +01:00
Joost Lekkerkerker 91ebeb84e7 Add integration_type service to monzo (#159697) 2025-12-25 21:52:54 +01:00
Joost Lekkerkerker 1664dd5702 Add integration_type device to mikrotik (#159691) 2025-12-25 21:52:37 +01:00
Joost Lekkerkerker 5e96ec820f Add integration_type service to monarch_money (#159695) 2025-12-25 21:50:17 +01:00
Joost Lekkerkerker 5eedef4920 Add integration_type hub to monoprice (#159696) 2025-12-25 21:50:03 +01:00
Joost Lekkerkerker 71728ba37e Add integration_type device to moat (#159693) 2025-12-25 21:48:54 +01:00
Allen Porter 5657bd11b8 Start reauth when roborock notices the MQTT session is unauthorized (#159719) 2025-12-25 21:47:42 +01:00
Joost Lekkerkerker cd6bb861a8 Add integration_type service to mutesync (#159701) 2025-12-25 13:53:33 +01:00
Joost Lekkerkerker 3175c149c6 Add integration_type device to mpd (#159699) 2025-12-25 13:53:20 +01:00
Joost Lekkerkerker bfe1e70e06 Add integration_type device to mystrom (#159703) 2025-12-25 13:52:33 +01:00
Joost Lekkerkerker 0a4c75951a Add integration_type device to nanoleaf (#159704) 2025-12-25 13:51:48 +01:00
Joost Lekkerkerker fb6380157a Add integration_type hub to neato (#159705) 2025-12-25 13:51:19 +01:00
Retha Runolfsson 8d2b925131 Add support for switchbot art frame (#159710) 2025-12-25 13:50:46 +01:00
Ville Skyttä 5359a8bf26 Tidy up various Huawei LTE sensor values for display (#159728) 2025-12-25 13:15:06 +01:00
melo be966f1196 Bump tuya-device-sharing-sdk to 0.2.7 (#159734) 2025-12-25 11:53:56 +01:00
Kamil Breguła 2d6ae8f907 Add sensors to Google Drive (#156167)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-24 21:24:36 -08:00
J. Nick Koston 2683b893c4 Bump aioesphomeapi to 43.6.0 (#159664) 2025-12-24 13:28:36 -10:00
Galorhallen 2b5de0db01 Update govee-local-api 2.3.0 (#159721) 2025-12-24 21:47:28 +01:00
Allen Porter 83d4f8eedc Fix Roborock repair issue behavior (#159718) 2025-12-24 21:00:36 +02:00
Joost Lekkerkerker 89247e9069 Add integration_type hub to mysensors (#159702) 2025-12-24 19:54:56 +01:00
Allen Porter 5f996e700f Bump python-roborock to 3.21.1 (#159660) 2025-12-24 09:27:41 -08:00
cdnninja 8f44eb6652 Improve VeSync startup error handling (#158126) 2025-12-24 18:17:16 +01:00
Joost Lekkerkerker 31dadd3102 Add integration_type service to mullvad (#159700) 2025-12-24 13:50:34 +01:00
Joost Lekkerkerker 4c74a57b63 Add integration_type service to minecraft_server (#159692) 2025-12-24 13:50:02 +01:00
Vincent Wolsink 750744f332 Fix display of target_humidity in Huum (#159683) 2025-12-24 13:49:31 +01:00
Joost Lekkerkerker 03442c5e51 Add integration_type hub to nest (#159706) 2025-12-24 13:47:57 +01:00
Matthias Alphart 26774c20c7 Update knx-frontend to 2025.12.24.74016 (#159678) 2025-12-24 12:51:55 +01:00
Matthias Alphart 29201ac5d6 Fix anglian water test snapshot (#159684) 2025-12-24 13:10:34 +02:00
Matthias Alphart a6938127ea Fix inels config flow tests (#159688) 2025-12-24 13:08:20 +02:00
Retha Runolfsson 65d7f22072 Bump pySwitchbot to 0.75.0 (#159685) 2025-12-24 12:57:50 +02:00
dafal 0730c707e9 Bump bthome-ble to 3.17.0 (#159681) 2025-12-24 12:53:49 +02:00
Paul Tarjan 2e3eb0f9af Add uv.lock to .gitignore (#158754) 2025-12-24 00:06:59 +01:00
Ville Skyttä 2b5823c264 Huawei LTE sensor dynamic icon improvements (#159611) 2025-12-23 23:46:15 +01:00
cdnninja 95165022db Adjust vesync to follow action-setup (#157795) 2025-12-23 22:11:34 +01:00
Manuel Stahl 7c71c0377f Remove deprecated import from stiebel_eltron (#158110) 2025-12-23 22:05:22 +01:00
Jordan Harvey b07b699e79 Add account selector to Anglian Water config flow (#158242) 2025-12-23 22:04:54 +01:00
Paul Tarjan 34db548725 Change Samsung TV WoL turn_on log from warning to debug (#158676) 2025-12-23 22:01:44 +01:00
Pete Sage 5150efd63f Create issue for Sonos when Sonos system does not have UPnP enabled (#159330) 2025-12-23 22:00:28 +01:00
Louis Christ 0525c75686 Support media player grouping in bluesound integration (#159455)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-23 21:48:28 +01:00
Kamil Breguła 7c14862f62 Normalize unique ID in WLED (#157901)
Co-authored-by: mik-laj <12058428+mik-laj@users.noreply.github.com>
2025-12-23 21:29:20 +01:00
Rene Nulsch 19f8d9d41b Fix ZeroDivisionError for inverse unit conversions (#159161) 2025-12-23 21:25:19 +01:00
Andrew Jackson af1218876c Add Transmission get_torrents service and codeowner (#159211)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 21:11:47 +01:00
Lukas Gill 9715a7cc32 Add light level data to switchbot presence sensor (#159356) 2025-12-23 20:57:41 +01:00
epenet b87b72ab01 Repair flow description placeholders are optional (#159385) 2025-12-23 20:31:04 +01:00
Jan-Philipp Benecke 0f3f16fabe Remove migration of wrong encoded folder path from WebDAV (#159457) 2025-12-23 20:30:24 +01:00
W7RZL 85311e3def Add solar production sensors to neurio_energy (#159533) 2025-12-23 20:26:43 +01:00
epenet a33a4b6d9d Deprecate pyserial-asyncio in requirements manager (#159368)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-23 20:25:41 +01:00
Petro31 02f412feb1 Update template sensor tests to use new framework (#159466)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-23 20:21:57 +01:00
Raphael Hehl b3c78d4207 Improve date handling in UniFi Protect media source (#159491)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-23 20:21:21 +01:00
Sab44 a3dec29c72 Add Computer Name to device in Libre Hardware Monitor (#159342) 2025-12-23 19:54:35 +01:00
starkillerOG aa20a74a76 Bump reolink_aio to 0.18.0 (#159649) 2025-12-23 20:24:24 +02:00
Matthias Alphart c0fa6ad2e0 Support KNX scene entity configuration from UI (#159494)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 18:56:02 +01:00
Raphael Hehl 5107b7012d Add helper utility for patching Pydantic model methods in UniFi Protect tests (#159346)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-23 18:54:56 +01:00
Duco Sebel bcc5985c8b Enable HomeWizard Battery group mode by default when device controls batteries (#159493)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-23 18:54:28 +01:00
Marcello 5933c09a1d Add Fluss+ Button integration (#139925)
Co-authored-by: NjDaGreat <1754227@students.wits.ac.za>
Co-authored-by: NjeruFluss <161302608+NjeruFluss@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-23 18:48:22 +01:00
Robin Lintermann 5f1e6f3633 Bump pysmarlaapi to 0.9.3 (#159638) 2025-12-23 15:27:10 +01:00
Jan Klausa 6bd8d123ed Add support for SwitchBot Ceiling Lights (#159072) 2025-12-23 15:25:22 +01:00
Ville Skyttä 50a51b5ecc Improve upnp sensor icons (#159496) 2025-12-23 14:12:49 +01:00
wollew c115b418ac Handle auth errors in velux integration and add reauth flow (#159596)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-23 14:11:40 +01:00
Maikel Punie 2160827a50 Refactor Velbus sensors (#159600)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-23 13:52:08 +01:00
wollew 82d84d7adf raise HomeAssistantError when velux gateway reboot fails (#159585)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-23 13:41:49 +01:00
Maikel Punie 3e498d289b Velbus make sure the services throw exceptions (#159583) 2025-12-23 13:39:16 +01:00
mettolen e6d8092c37 Add binary sensors to Saunum integration (#159608) 2025-12-23 13:35:27 +01:00
mettolen 2e4f95c099 Add number entity to Airobot integration (#159595) 2025-12-23 13:33:46 +01:00
Ville Skyttä 9f54b09423 Do not create Huawei LTE sensors having None values (#159612) 2025-12-23 13:31:07 +01:00
Allen Porter 8361d65d23 Bump python-roborock to 3.20.1 (#159621) 2025-12-23 12:59:47 +02:00
Marc Mueller 7a82aa4803 Revert "Exempt pyparsing from license check (#159605)" (#159631) 2025-12-23 12:58:56 +02:00
Artur Pragacz 02ab11c1bd Mark entities as unavailable in Onkyo (#159521) 2025-12-23 08:39:34 +01:00
Brett Adams 64f0a615df Bump teslemetry-stream to 0.9.0 (#159617) 2025-12-22 17:21:43 -08:00
Craig Callender 3e889616f2 Remove 'hair_pinning' from Tailscale (#156728)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-22 23:47:10 +00:00
Aviad Levy bdbe2a6346 Fix allowlist dir requirement in download file handling for Telegram bot (#159615) 2025-12-23 00:09:00 +01:00
Aviad Levy 016d492342 Add download file service to Telegram bot integration (#154625)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-22 23:12:51 +01:00
karwosts 9ce46c0937 Redesign frontend.set_theme service form (#157866)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-22 23:03:06 +01:00
Robert Svensson 8d96aee96e Bump axis to v66 fixing an issue with latest xmltodict (#159604)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-22 21:51:44 +00:00
Mary 7083a0fdb7 Fix typo in test names (exception) (#159591) 2025-12-22 21:21:36 +01:00
Mary e3976923b2 Fix test name typo (trailing underscore) (#159592) 2025-12-22 21:16:01 +01:00
Mary 0b20417895 Fix Ecoforest unknown alarm translation key (#159594) 2025-12-22 21:08:27 +01:00
Jordan Harvey ed46c30b10 Bump pynintendoparental to 2.3.0 (#159571)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-22 21:05:09 +01:00
Ross Patterson 38f4cf0575 Clean up docstring copied word typo (#159581) 2025-12-22 21:04:45 +01:00
dontinelli 7b60cc3a80 Disable quoted cookies for compatibility with older SolarLog devices (#157839) 2025-12-22 19:41:46 +01:00
TheJulianJES fe0c92b6c5 Exempt pyparsing from license check (#159605) 2025-12-22 18:47:02 +01:00
Erik Montnemery c4386b4360 Add additional numerical climate triggers (#159471) 2025-12-22 17:36:27 +00:00
Erik Montnemery d4d26bccc1 Add numerical humidifier triggers (#159472)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-22 17:30:06 +00:00
Raphael Hehl 550b7bf7ba Bump uiprotect to 7.33.3 (#159593) 2025-12-22 17:44:18 +01:00
Erik Montnemery 6ff472ff87 Add light brightness triggers (#159473) 2025-12-22 15:54:53 +00:00
Marc Hörsken ca30d8b1c2 Add support for load switches to WMS WebControl pro (#151047)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-22 15:12:28 +01:00
Maikel Punie aae98a77d5 Bump valbusaio to 2025.12.0 (#159578) 2025-12-22 14:44:46 +01:00
Matrix 30b7b24ddd Bump yolink-api to 0.5.9 (#159587) 2025-12-22 14:19:04 +01:00
wollew a972a6d43a Make velux rain sensor unavailable if update fails (#159520)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-22 14:06:54 +01:00
Robert Resch 6e06c015df Bump go2rtc-client to 0.4.0 (#159516) 2025-12-22 12:25:47 +01:00
wollew 01c3e88e0f provide Squeezebox player sensor for next alarm timestamp (#155788) 2025-12-22 11:53:38 +01:00
Magnus fd9064376a Bump melissa to 3.0.3 (#159557) 2025-12-22 09:08:03 +01:00
dependabot[bot] 9eb5d452cf Bump docker/setup-buildx-action from 3.11.1 to 3.12.0 (#159577)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 09:04:01 +01:00
J. Nick Koston 966209e4b6 Bump aioesphomeapi to 43.4.0 (#159524) 2025-12-21 21:25:23 +01:00
Frank a09ac94db9 Correct spelling of property (#159549) 2025-12-21 21:22:28 +01:00
Allen Porter 0710cf3e6b Redact additional unnecessary diagnostic fields (#159546) 2025-12-21 09:50:51 -08:00
Joakim Plate a81f2a63c0 Ensure all base component dependencies are added (#157428) 2025-12-21 15:24:56 +01:00
Manu 6ef2d0d0a3 Add integration type hub to Xbox (#159528) 2025-12-21 07:59:03 +01:00
Manu 911ea67a6d Change integration type to hub in PlayStation Network (#159529) 2025-12-21 07:58:49 +01:00
Josef Zweck 28dc32d5dc Follow through with deprecation in async_config_entry_first_refresh (#158775) 2025-12-21 07:56:35 +01:00
Abílio Costa c95416cb48 Add scene activated trigger (#159226)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-12-21 01:07:00 +00:00
wollew 7dc9084f06 Velux action setup (#159502) 2025-12-20 19:49:15 +01:00
Svetoslav 39ba36d642 Fix syntax error in mute_volume method (#159458) 2025-12-20 19:45:02 +01:00
Álvaro Fernández Rojas 5009560f57 Update aioqsw to v0.4.2 (#159467) 2025-12-20 19:43:20 +01:00
Niracler 41e88573bb Enhance Sunricher DALI with stale-device cleanup (#156015)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-20 18:26:57 +01:00
Markus Jacobsen 27ee986b1b Add Beoremote One diagnostics to Bang & Olufsen (#159447) 2025-12-20 18:25:04 +01:00
Lukas c9d21c1851 Pooldose: Add parallel updates (Silver Qly Scale) (#159479) 2025-12-20 18:23:25 +01:00
wollew 2afbdc5757 add gateway disconnect on unload of velux integration (#159497) 2025-12-20 18:16:58 +01:00
Joost Lekkerkerker 14cb8af9fe Add integration_type service to meteoclimatic (#159488) 2025-12-20 15:16:31 +01:00
Joost Lekkerkerker 74ae0f8297 Add integration_type service to metoffice (#159489) 2025-12-20 15:14:18 +01:00
Paul Tarjan 3050a5c896 Support NVR Hikvision devices (#159253) 2025-12-20 10:08:48 +01:00
Raphael Hehl 9f886e66c7 Update UniFi Protect select entities to use snake_case state values with proper translations (#159284)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-12-20 10:07:21 +01:00
Tom Harris 3c752d4516 Bump insteon panel to 0.6.0 to fix dialog button issues (#159449) 2025-12-20 10:05:03 +01:00
Raphael Hehl e4bfdf5b30 Add quality scale configuration for UniFi Protect integration (#157568)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-20 10:03:02 +01:00
Artur Pragacz 3e43424a73 Add myself as codeowner to intent script (#159454) 2025-12-20 10:00:58 +01:00
Matthias Alphart 0db9dcfd1c Fix knx translation typos (#159486) 2025-12-20 09:53:45 +01:00
J. Nick Koston 5b5850224a Bump yalexs-ble to 3.2.4 (#159476) 2025-12-19 14:05:07 -10:00
Erik Montnemery 065b0eb5b2 Fix siren entity triggers (#159474) 2025-12-19 22:45:32 +01:00
Michael 6a1d86d5db Add domain driven triggers to lock platform (#159327)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 22:34:33 +01:00
Petro31 f99a73ef28 Modernize template weather platform and add config flow (#156399) 2025-12-19 22:28:26 +01:00
Michael 0436d30062 Add turned off and turned on triggers to siren platform (#158847)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 22:15:06 +01:00
Erik Montnemery 24b6b5452b Add trigger climate.target_temperature_crossed_threshold (#159461)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-19 21:57:10 +01:00
Erik Montnemery 8b91ebfe30 Add test of error handling in numerical_attribute_changed triggers (#159469) 2025-12-19 21:40:56 +01:00
Matthias Alphart 37d3b73c1b Support KNX sensor entity configuration from UI (#158498) 2025-12-19 19:20:14 +01:00
Matthias Alphart c881d9809e Update knx-frontend to 2025.12.19.150946 (#159446) 2025-12-19 19:09:19 +01:00
Bram Kragten 62ec64c3fe 2025.12.4 (#159460) 2025-12-19 18:54:49 +01:00
Erik Montnemery 85dfe3a107 Add trigger climate.target_temperature_changed (#159434) 2025-12-19 18:39:53 +01:00
Bram Kragten cbc6306963 Merge branch 'master' into rc 2025-12-19 18:27:05 +01:00
Pierre PÉRONNET d8a468833e Bump renault-api to 0.5.2 (#159448) 2025-12-19 18:25:46 +01:00
Bram Kragten e098acfa69 Bump version to 2025.12.4 2025-12-19 18:12:22 +01:00
Raphael Hehl 5bbd56b8e6 Add exception handling to UniFi Protect entity commands (#159292)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-19 07:10:32 -10:00
Bram Kragten 52630ccca1 Update frontend to 20251203.3 (#159451) 2025-12-19 18:10:28 +01:00
Robert Resch 3001dcb8ff Remove users refresh tokens when the user get's deactivated (#159443) 2025-12-19 18:10:27 +01:00
Allen Porter cec5134369 Bump python-roborock to 3.19.0 (#159404) 2025-12-19 18:10:26 +01:00
puddly 80f2889e1f Bump ZHA to 0.0.81 (#159396) 2025-12-19 18:10:25 +01:00
Simone Chemelli 188c98fd08 Align format of voltmeter strings for Shelly (#159394) 2025-12-19 18:10:25 +01:00
Artur Pragacz e086e013d5 Do not trigger reauth for addon in Music Assistant (#159372) 2025-12-19 18:10:24 +01:00
Simone Chemelli 3c20df961e Add missing strings for Shelly voltmeter sensor (#159332) 2025-12-19 18:10:23 +01:00
Allen Porter 9f31d95940 Fix AttributeError in Roborock Empty Mode entity (#159278)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 18:10:22 +01:00
Andre Lengwenus d5cbc6efca Bump pypck to 0.9.8 (#159277) 2025-12-19 18:10:21 +01:00
Luke Lashley 793877bfeb Bump python-roborock to 3.18.0 (#159271) 2025-12-19 18:10:21 +01:00
Andre Lengwenus 692847d9a8 Fix incorrect status updates for lcn (#159251) 2025-12-19 18:10:19 +01:00
Richard Polzer 31785bf68f Bump ekey-bionyxpy to version 1.0.1 (#159196) 2025-12-19 18:10:18 +01:00
Åke Strandberg d17ed3ed95 Handle missing Miele status codes gracefully (#159124) 2025-12-19 18:10:17 +01:00
Pete Sage 7bbeb2a006 Bump soco to 0.30.13 for Sonos (#159123) 2025-12-19 18:10:16 +01:00
Jordan Harvey 7275be4629 Bump pynintendoparental 2.1.3 (#159120)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-12-19 18:10:16 +01:00
Pete Sage 37a32bf27d Sonos increase wait for groups timeout (#159108) 2025-12-19 18:10:14 +01:00
Pete Sage 00b7138c43 Sonos fix media player join to avoid race condition (#159106) 2025-12-19 18:10:13 +01:00
PaulCavill 1b464e799b Improve icloud reauth flow (#159081) 2025-12-19 18:10:12 +01:00
TimL 1a56855158 Bump pysmlight to v0.2.13 (#159075)
Co-authored-by: Tim Lunn <tim@feathertop.org>
2025-12-19 18:10:11 +01:00
Bram Kragten 0dac52cbe4 Bump aiodns to 3.6.1 (#159073) 2025-12-19 18:09:13 +01:00
Allen Porter 63cb220a8f Fix slow event state updates for remote calendar (#159058) 2025-12-19 18:02:13 +01:00
Kevin Fronczak af72bc4d2a Bump blinkpy to 0.25.2 (#159049) 2025-12-19 18:02:12 +01:00
Xidorn Quan 108d94ab06 Bump aioasuswrt to 1.5.4 (#159038) 2025-12-19 18:02:11 +01:00
Allen Porter d64313cd28 Add exception handling for rate limited or unauthorized MQTT requests (#158997) 2025-12-19 18:02:10 +01:00
Petro31 b608dcb2eb Update unnecessary error logging of unknown and unavailable source states from mold indicator (#158979) 2025-12-19 18:02:10 +01:00
Allen Porter e0fa5db218 Bump ical to 12.1.2 (#158965) 2025-12-19 18:02:09 +01:00
Jan Bouwhuis 96d2ecf250 Assume cover or valve is always "running" in google assistant when the state is assumed or the position is reported to allow it to be be stopped (#158919) 2025-12-19 18:02:08 +01:00
Aidan Timson b0fac94666 Update systembridgeconnector to 5.2.4, fix media source (#158917) 2025-12-19 18:02:07 +01:00
Andrew Jackson 8902ba9f1d Bump aiomealie to 1.1.1 and statically define mealplan entry types (#158907) 2025-12-19 18:02:06 +01:00
Bouwe Westerdijk 581919ccb4 Revert adding entity_category to Plugwise thermostat schedule select (#158901) 2025-12-19 18:02:05 +01:00
Magnus 7714b51c21 Bump aioasuswrt 1.5.3 (#158882) 2025-12-19 18:02:04 +01:00
Jordan Harvey 8ee94f829a Bump pynintendoparental to 2.1.1 (#158779) 2025-12-19 18:02:03 +01:00
Paul Tarjan 73734d2ff2 Fix Sonos speaker async_offline assertion failure (#158764) 2025-12-19 18:02:02 +01:00
Paul Tarjan b7d4c3c5d1 Suppress verbose UPnP subscription error logs (#158677) 2025-12-19 18:02:01 +01:00
Allen Porter 5d30fc3436 Suppress roborock failures under some unavailability threshold (#158673)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 18:02:00 +01:00
Jordan Harvey 4cced81f86 Update pynintendoparental to 2.1.0 (#158487) 2025-12-19 18:01:58 +01:00
Thomas D 81d10d02de Enable volvo engine status for all engine types (#158437) 2025-12-19 18:01:57 +01:00
Jordan Harvey 73484cb8fb Update pynintendoparental to 2.0.0 (#158285) 2025-12-19 18:01:56 +01:00
starkillerOG d0aaac0382 Do not check Reolink firmware at start (#158275) 2025-12-19 18:01:55 +01:00
Federico Imberti 67550731b3 Prevent empty aliases in registries (#156061)
Co-authored-by: J. Diego Rodríguez Royo <jdrr1998@hotmail.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-12-19 18:01:54 +01:00
Bram Kragten d0411b6613 Update frontend to 20251203.3 (#159451) 2025-12-19 17:57:27 +01:00
Abílio Costa 293fbebef2 Modernize calendar trigger (#159395)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2025-12-19 17:41:30 +01:00
Erik Montnemery cfe542acb9 Fix hassfest support for choose selector translations (#159453) 2025-12-19 17:31:57 +01:00
Bram Kragten 8da323d4b7 Add support for choose selector (#159412) 2025-12-19 16:49:04 +01:00
Zoltán Farkasdi b2edf637cc Netatmo camera webhook refactor (#159359) 2025-12-19 16:41:22 +01:00
Erik Montnemery de61a45de1 Add humidifier triggers (#159163) 2025-12-19 16:38:30 +01:00
Erik Montnemery d9324cb0e4 Improve docstrings in climate trigger tests (#159438) 2025-12-19 16:10:07 +01:00
Robert Resch 4a464f601c Remove users refresh tokens when the user get's deactivated (#159443) 2025-12-19 15:50:47 +01:00
Thomas D 43e9c24c18 Adjust volvo update interval (#159200) 2025-12-19 15:38:50 +01:00
Matthias Alphart 1c3492b4c2 KNX Fan: Add support for switch addresses (#159367) 2025-12-19 15:37:50 +01:00
johanzander e0cb56a38c Improve Growatt Server config flow with region dropdown (#159329)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-19 15:33:35 +01:00
Erik Montnemery 6e05cc4898 Enable multiple states in trigger climate.hvac_mode_changed (#159435) 2025-12-19 15:14:55 +01:00
MoonDevLT 6f9dc2e5a2 Add a DALI line into the device hierarchy with a broadcast entity (#156570)
Co-authored-by: Tom <CoMPaTech@users.noreply.github.com>
2025-12-19 14:57:51 +01:00
Petro31 ddb1ae371d Add new template entity framework to template alarm control panel (#156614) 2025-12-19 14:41:45 +01:00
J. Diego Rodríguez Royo 6553337b79 Add entities related to the new data from aiohomeconnect 0.22.0 (#154717) 2025-12-19 14:33:28 +01:00
Thomas55555 aedc729d57 Only allow unique location names in google air quality (#159285) 2025-12-19 14:33:16 +01:00
Erik Montnemery 31fa69b609 Fix evict_faked_translations fixture (#159419)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-19 14:00:58 +01:00
Brett Adams b819a866b9 Bump tesla-fleet-api to 1.3.2 (#159430) 2025-12-19 13:19:45 +01:00
Erik Montnemery 6cc7d83def Add trigger climate.hvac_mode_changed (#159358) 2025-12-19 12:57:01 +01:00
Joost Lekkerkerker 5154418051 Add integration_type device to incomfort (#159173) 2025-12-19 12:34:16 +01:00
Josef Zweck 7e63c12b95 Add entity picture to lamarzocco (#158518) 2025-12-19 11:59:51 +01:00
puddly d17e951591 Bump ZHA to 0.0.81 (#159396) 2025-12-19 10:27:50 +01:00
dependabot[bot] 9198e5f56d Bump actions/attest-build-provenance from 3.0.0 to 3.1.0 (#159405)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-19 10:06:42 +01:00
Ludovic BOUÉ 97d7e0e01e Matter Speaker volume LevelControl (#149490)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-19 10:01:55 +01:00
Ravaka Razafimanantsoa 4d5b8c4b08 Bump momonga to 0.3.0 (#159350) 2025-12-19 08:35:05 +01:00
martinkiska abb011311e bump nibe to 2.20.0 (#159392) 2025-12-19 08:33:48 +01:00
Allen Porter 92cf7623fa Bump python-roborock to 3.19.0 (#159404) 2025-12-19 08:33:34 +01:00
Allen Porter aedf4c881b Fix AttributeError in Roborock Empty Mode entity (#159278)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-18 20:09:22 -08:00
Lukas 74baf44c83 Pooldose: Add select platform (#159240) 2025-12-19 00:13:26 +01:00
Raphael Hehl 9afb4a9eb8 Improve UniFi Protect test quality and fixtures (#159316)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-18 22:23:44 +00:00
Simone Chemelli e1967bef9a Align format of voltmeter strings for Shelly (#159394) 2025-12-18 23:22:43 +01:00
Paul Tarjan f17b6aa9e4 Bump pyHik to 0.3.4 (#159380) 2025-12-18 20:28:13 +01:00
Paul Tarjan dd6d7397d9 Suppress verbose UPnP subscription error logs (#158677) 2025-12-18 20:02:48 +01:00
Paul Tarjan aeabd2d2cc Add @ptarjan as code owner for hikvision integration (#159381) 2025-12-18 19:58:48 +01:00
Ludovic BOUÉ d7af2f39c2 Move Matter DoorLock mode selection in control section (#158920) 2025-12-18 19:51:14 +01:00
Keir Stiegler a674ad11bc Map Z-Wave Jasco model 14314 fan speed to low/medium/high (#155817)
Co-authored-by: Robert Resch <robert@resch.dev>
2025-12-18 19:50:53 +01:00
Artur Pragacz ccb64d7fd8 Add integration type to workday (#157731)
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-18 19:50:10 +01:00
Matthias Alphart 36691e2a3d Update KNX quality scale to platinum (#159379) 2025-12-18 19:47:30 +01:00
Paul Tarjan 8971f75f13 Fix Sonos speaker async_offline assertion failure (#158764) 2025-12-18 19:46:39 +01:00
Duco Sebel 173db170af Remove 'energy' name from HomeWizard (#159089) 2025-12-18 19:46:07 +01:00
Jordan Harvey 881851a4f6 Add statistics importing for Anglian Water (#157757) 2025-12-18 19:45:24 +01:00
MarkGodwin 4b4b64e939 Achieve Bronze quality rating for TP-Link Omada (#156697) 2025-12-18 19:44:08 +01:00
rlippmann e721c1a092 Simplisafe: Trigger binary sensor from secret alerts (#156848)
Co-authored-by: Aaron Bach <bachya1208@gmail.com>
2025-12-18 19:42:55 +01:00
Joost Lekkerkerker 0933c9fe51 Add integration_type device to hyperion (#159139) 2025-12-18 19:31:43 +01:00
Joost Lekkerkerker 632b3e5dc3 Add integration_type service to huisbaasje (#159133) 2025-12-18 19:31:15 +01:00
Joost Lekkerkerker 434cb48344 Add integration_type device to gogogate2 (#159082) 2025-12-18 19:30:36 +01:00
Joost Lekkerkerker 86d4c3cbbf Add integration_type hub to freebox (#159023) 2025-12-18 19:30:10 +01:00
Joost Lekkerkerker 3019f9041c Add integration_type hub to enocean (#159005) 2025-12-18 19:29:32 +01:00
Hans Fehrmann 9e0a3dee08 Enable name alias when sending a notification for google_mail (#157927)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-18 19:28:23 +01:00
Joost Lekkerkerker fefe7d9e5d Add integration_type device to hisense_aehw4a1 (#159125) 2025-12-18 19:26:39 +01:00
Joost Lekkerkerker 4c382cedff Add integration_type hub to libre_hardware_monitor (#159301) 2025-12-18 19:24:58 +01:00
Joost Lekkerkerker 6ffd05313b Add integration_type device to lookin (#159304) 2025-12-18 19:24:26 +01:00
Joost Lekkerkerker 9be0214021 Add integration_type hub to lutron_caseta (#159308) 2025-12-18 19:23:53 +01:00
epenet 54300430b7 Use common read_device_status method in Tuya light wrapper (#159156) 2025-12-18 19:14:23 +01:00
Paul Tarjan a25038259e Fix generic camera preview stream URL to be absolute (#159113)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-18 19:14:13 +01:00
Kurt Chrisford 81be14c8f1 Actron Air: Add switch entity platform (#158087)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-18 19:11:39 +01:00
airwoflgh 62464b83dc Add preset default to radiotherm (#159335)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-18 19:07:45 +01:00
epenet beb909528c Use common options attribute in Tuya cover wrapper (#159147) 2025-12-18 19:00:57 +01:00
Richard ef28715360 Mill: Add ability to set heating device to AUTO (#157745) 2025-12-18 19:00:30 +01:00
ashalita 78cc41fdc0 CoolMasterNet: Send wakeup prompt (#156116) 2025-12-18 18:59:35 +01:00
Matthias Alphart 6a868ca5cc Add repair issue for KNX DataSecure key issues (#157843) 2025-12-18 18:23:30 +01:00
Joost Lekkerkerker f43dead38c Add temperature entities to SmartThings One Door fridge (#158457) 2025-12-18 18:19:01 +01:00
Petro31 86163252e1 Update template switch tests to use new framework (#159215) 2025-12-18 18:10:43 +01:00
Petro31 0cd5202596 Update template update tests to use new framework (#159207) 2025-12-18 18:09:32 +01:00
Anton Dalgren 33dcde7de1 Add sensor platform for AirPatrol (#158726) 2025-12-18 18:00:58 +01:00
Bouwe Westerdijk c449b2e2e8 Improve Plugwise coordinator code (#158983) 2025-12-18 18:00:42 +01:00
Matthias Alphart f40f7072c8 Update xknx to 3.13.0 (#159371) 2025-12-18 16:45:40 +00:00
Abílio Costa 4163ecd833 Improve typing for get_x_for_target commands (#159279) 2025-12-18 16:42:40 +00:00
Niracler 9c59d528af Add scene platform for Sunricher DALI integration (#157808)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-12-18 17:42:30 +01:00
theobld-ww c2440c4ebd Add Watts Vision + integration with tests (#153022)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-18 17:41:23 +01:00
Anthony Garera cb275f65ba Adding AmGarera as a code owner for Overseerr integration (#159373) 2025-12-18 17:36:00 +01:00
Paul Tarjan b1923df3ca Pass ssl parameter to pyhik HikCamera (#159256) 2025-12-18 17:35:55 +01:00
Paul Tarjan 7ddfd155ca Fix hikvision camera.get_id (#159257) 2025-12-18 17:17:27 +01:00
Paul Tarjan e01df6d10d Add more docs to Withings webhook log (#158748) 2025-12-18 16:50:23 +01:00
Artur Pragacz 54010728d5 Do not trigger reauth for addon in Music Assistant (#159372) 2025-12-18 16:49:01 +01:00
Kurt Chrisford 62a3b3827f Actron Air Integration: Fix fan mode mapping and update actron-neo-api requirement (#159195) 2025-12-18 16:48:00 +01:00
Joost Lekkerkerker b9abfba20f Add integration_type service to met_eireann (#159314)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-12-18 16:32:09 +01:00
PaulCavill eca9f36e55 Improve icloud reauth flow (#159081) 2025-12-18 16:01:20 +01:00
Matthias Alphart 3c865c6f41 Support KNX fan entity configuration from UI (#159167) 2025-12-18 15:54:55 +01:00
Raphael Hehl 3b32c4bcbf Remove custom device_class from unifiprotect doorbell_text select entity (#159366)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-18 15:51:16 +01:00
dependabot[bot] fcdc1cfed9 Bump github/codeql-action from 4.31.8 to 4.31.9 (#159248)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-18 15:45:47 +01:00
wollew 0fd782c4ab Raise exception if velux integration setup fails because of connection erros (#159231) 2025-12-18 15:44:42 +01:00
adam-the-hero bbcaf69973 Bump quality scale for watergate to silver (#155353) 2025-12-18 15:30:15 +01:00
Denis Shulyaka f2b713acac Exclude gpt-4o model from extended caching (#159362) 2025-12-18 08:48:01 -05:00
LG-ThinQ-Integration 6c944d6b15 Adds a delay to the continuous control of the climate (#151177)
Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-12-18 14:46:11 +01:00
Raphael Hehl 4dd3abb16a Fix device classes in unifiprotect integration (#159281)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2025-12-18 14:43:44 +01:00
adam-the-hero d2672b9ddf Introduce session inject to watergate integration (#159360) 2025-12-18 14:08:57 +01:00
Matthias Alphart ff30492919 KNX unit tests: patch CEMIHandler at class level (#159317) 2025-12-18 14:02:02 +01:00
Duco Sebel b5ccdf8165 Implement new battery charge modes in HomeWizard (#159107) 2025-12-18 14:01:37 +01:00
Robert Resch b3c745cfa7 Bump go2rtc to 1.9.13 (#159043) 2025-12-18 14:00:50 +01:00
Robert Resch 67aeafa797 Add advanced section for generic camera config flow (#148430) 2025-12-18 13:30:25 +01:00
Michael 3d71b6de44 Add support for FRITZ! Smarthome routines (#158947) 2025-12-18 13:09:06 +01:00
Luke Lashley 5349045932 Add basic support for Q7 devices (#159274) 2025-12-18 12:30:20 +01:00
epenet 4960871c84 Revert name change in meteo_france (#159352) 2025-12-18 11:04:34 +01:00
epenet af3861cd6b Rename attribute in Tuya climate wrapper (#159145) 2025-12-18 10:02:38 +01:00
epenet f9a070e9b3 Use common options attribute in Tuya event wrapper (#159119) 2025-12-18 09:33:53 +01:00
epenet fd503b2e33 Make VacuumEntityFeature.STATE conditional in Tuya vacuum (#159254) 2025-12-18 09:32:13 +01:00
Franck Nijhof 04746b6843 2025.12.3 (#158811) 2025-12-12 19:10:33 +01:00
Magnus 0547153730 Bump aioasuswrt to 1.5.2 (#158727) 2025-12-12 17:37:17 +00:00
Franck Nijhof eb024b4dde Bump version to 2025.12.3 2025-12-12 17:23:29 +00:00
Joost Lekkerkerker 1d4817608e Bump pySmartThings to 3.5.1 (#158795) 2025-12-12 17:23:16 +00:00
Manu a37ca293e1 Increase Xbox update interval to 15 seconds and refactor title data handling (#158780) 2025-12-12 17:23:15 +00:00
Josef Zweck f3dbddee16 Bump pylamarzocco to 2.2.4 (#158774) 2025-12-12 17:20:51 +00:00
Josef Zweck b26681ee88 Bump pylamarzocco to 2.2.3 (#158104) 2025-12-12 17:20:49 +00:00
Allen Porter effe72bfda Bump ical to 12.1.1 (#158770) 2025-12-12 17:19:13 +00:00
cdutr 076835ca1c Migrate Blink component to use hardware_id instead of device_id (#158765) 2025-12-12 17:19:12 +00:00
Thomas55555 4b9b1e611a Bump google air quality api to 2.0.2 (#158742) 2025-12-12 17:19:11 +00:00
ndrwrbgs 0b4ea42810 Update advanced_options display text for MQTT (#158728) 2025-12-12 17:19:09 +00:00
johanzander 8907608345 Add state_class to Growatt power and energy sensors (#158705)
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-12 17:19:08 +00:00
J. Nick Koston 356ee07e22 Pin pycares to 4.11.0 (#158695) 2025-12-12 17:19:07 +00:00
Allen Porter bee3ee6320 Bump python-roborock to 3.12.2 (#158572) 2025-12-12 17:19:05 +00:00
Andrew Jackson fb72ff9bd0 Add measurement state class to ohme sensors (#158541) 2025-12-12 17:19:04 +00:00
bestycame 412e05d8da Bump hanna-cloud to version 0.0.7 (#158536)
Co-authored-by: Olivier d'Otreppe <odotreppe@abbove.com>
2025-12-12 17:19:03 +00:00
Yevhenii Vaskivskyi 58ee8e863e Bump asusrouter to 1.21.3 (#158492) 2025-12-12 17:19:01 +00:00
Ludovic BOUÉ e3a47bfc51 Fix Matter Door Lock Operating Mode select entity (#158468) 2025-12-12 17:19:00 +00:00
Allen Porter a6cdacc8fe Improve Roborock exception logging behavior for Zeo/Dyad devices (#158465)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 17:18:58 +00:00
epenet dd0425ab8e Add Tuya local_strategy to Tuya diagnostic (#158450) 2025-12-12 17:18:57 +00:00
Samuel Xiao 1d289c0083 Switchbot Cloud: Fixed binary sensors didn't update automatically (#158434)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 17:18:56 +00:00
Allen Porter 70786a1d90 Fix roborock off peak electricity timer (#158292) 2025-12-12 17:18:54 +00:00
Michel D'Astous 293eb69788 Fix webhook exception when empty json data is sent (#158254) 2025-12-12 17:18:53 +00:00
Kira 71d92291d1 Bump blinkpy to 0.25.1 (#158135)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-12-12 17:18:52 +00:00
Andre Lengwenus 726de64394 Bump pypck to 0.9.7 (#158089) 2025-12-12 17:18:50 +00:00
epenet de04f22f89 Improve Tuya HVACMode handling (#158042)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-12 17:18:49 +00:00
Jan Bouwhuis 9e8cc3a65b Move translatable URL out of strings.json for knx integration (#155244) 2025-12-12 17:04:30 +00:00
Franck Nijhof 27fa92b607 Fix Tuya BitmapTypeInformation parsing (#158475) 2025-12-10 17:06:50 +01:00
epenet ce5c5c5eb7 Fix Tuya BitmapTypeInformation parsing 2025-12-09 16:29:25 +00:00
Franck Nijhof 88e29df8eb 2025.12.2 (#158274) 2025-12-08 22:35:39 +01:00
Franck Nijhof a2b5744696 Bump version to 2025.12.2 2025-12-08 20:45:22 +00:00
Marcel van der Veldt 201c3785f5 Skip check for onboarding done in Music Assistant integration (#158270) 2025-12-08 20:17:05 +00:00
Paul Bottein 24de26cbf5 Update frontend to 20251203.2 (#158259) 2025-12-08 20:17:04 +00:00
andreimoraru ac0a544829 Bump yt-dlp to 2025.12.08 (#158253) 2025-12-08 20:17:03 +00:00
Petro31 1a11b92f05 Fix multiple top-level support for template integration (#158244) 2025-12-08 20:17:01 +00:00
epenet ab0811f59f Fix teslemetry service description placeholders (#158240) 2025-12-08 20:17:00 +00:00
epenet 68711b2f21 Fix yeelight service description placeholders (#158239) 2025-12-08 20:16:59 +00:00
epenet 886e2b0af1 Fix zwave_js service description placeholders (#158236) 2025-12-08 20:16:57 +00:00
Thomas55555 7492b5be75 Bump google air quality api to 2.0.0 (#158234) 2025-12-08 20:16:56 +00:00
Jan Bouwhuis e4f1565e3c Fix description placeholders for system_bridge (#158232) 2025-12-08 20:16:54 +00:00
Paul Bottein 7f37412199 Be more specific about winter mode in the description (#158230)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-08 20:16:53 +00:00
Allen Porter eaef0160a2 Bump python-roborock to 3.10.10 (#158212) 2025-12-08 20:16:52 +00:00
Harvey f049c425ba Bump HueBLE to 2.1.0 (#158197) 2025-12-08 20:16:50 +00:00
Yevhenii Vaskivskyi 50eee75b8f Bump asusrouter to 1.21.1 (#158192) 2025-12-08 20:16:48 +00:00
Åke Strandberg 81e47f6844 Bump pymiele dependency to 0.6.1 (#158177) 2025-12-08 20:16:46 +00:00
Åke Strandberg ffebbab020 Add program id codes for Miele WQ1000 (#158175) 2025-12-08 20:16:45 +00:00
Manu 9824bdc1c9 Fix secure URLs for promotional game media in Xbox integration (#158162) 2025-12-08 20:16:44 +00:00
Allen Porter a933d4a0eb Ensure Roborock disconnects mqtt on unload/stop (#158144)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-08 20:16:42 +00:00
Shay Levy f7f7f9a2de Revert "Remove Shelly redundant device entry check for sleepy devices" (#158108) 2025-12-08 20:16:41 +00:00
Petro31 aac412f3a8 Fix legacy template entity_id field in migration (#158105)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-08 20:16:39 +00:00
omrishiv 660a14e78d fix Lutron Caseta smart away subscription (#158082)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-12-08 20:16:38 +00:00
Franck Nijhof 4aa3f0a400 2025.12.1 (#158071) 2025-12-05 22:09:38 +01:00
Franck Nijhof 0b52c806d4 Bump version to 2025.12.1 2025-12-05 20:32:57 +00:00
Paul Bottein bbe27d86a1 Update frontend to 20251203.1 (#158069) 2025-12-05 20:32:28 +00:00
Raphael Hehl fb7941df1d Bump uiprotect to 7.33.2 (#158057) 2025-12-05 20:32:27 +00:00
Petro31 c46e341941 Fix inverted kelvin issue (#158054) 2025-12-05 20:32:25 +00:00
Jan Bouwhuis 2e3a9e3a90 Move example image path out of translatable strings (#158053) 2025-12-05 20:32:24 +00:00
Jan Bouwhuis 55c5ecd28a Move lametric URLs out of strings.json (#158051) 2025-12-05 20:32:22 +00:00
Denis Shulyaka e50e2487e1 Replace deprecated preview image model (#158048) 2025-12-05 20:32:21 +00:00
Maciej Bieniek 74e118f85c Do not create restart button for sleeping gen2+ Shelly devices (#158047) 2025-12-05 20:32:19 +00:00
Joost Lekkerkerker 39a62ec2f6 Prevent entsoe from loading (#158036)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
2025-12-05 20:32:18 +00:00
Petro31 1310efcb07 Fix missing template key in deprecation repair (#158033) 2025-12-05 20:32:16 +00:00
hanwg 53af592c2c Improve action descriptions for Telegram bot (#158022) 2025-12-05 20:32:15 +00:00
TheJulianJES 023987b805 Change ZHA strings for incorrect adapter state (#158021)
Co-authored-by: Norbert Rittel <norbert@rittel.de>
2025-12-05 20:32:13 +00:00
Allen Porter 5b8fb607b4 Bump python-roborock to 3.10.2 (#158020) 2025-12-05 20:32:12 +00:00
Mark Adkins 252f6716ff SharkIQ dep upgrade v1.5.0 (#158015) 2025-12-05 20:32:11 +00:00
Paul Tarjan bf78e28f83 Fix doorbird duplicate unique ID generation (#158013)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-12-05 20:32:09 +00:00
David Bonnes 22706d02a7 Bump evohome-async to 1.0.6 (#158005) 2025-12-05 20:32:08 +00:00
Abílio Costa 5cff0e946a Bump oralb-ble to 1.0.2 (#157992) 2025-12-05 20:32:06 +00:00
Luke Lashley 6cbe2ed279 Bump python-Roborock to 3.10.0 (#157980) 2025-12-05 20:32:04 +00:00
Paul Bottein fb0f5f52b2 Add subscribe preview feature endpoint to labs (#157976) 2025-12-05 20:32:03 +00:00
Jan Bouwhuis 5c422bb770 Move out example URL and IP of strings.json for reolink (#157970) 2025-12-05 20:32:01 +00:00
Jan Bouwhuis fd1bc07b8c Move pilight URL out of strings.json (#157967) 2025-12-05 20:31:59 +00:00
Petro31 97a019d313 Update template deprecation to be more explicit (#157965) 2025-12-05 20:31:58 +00:00
epenet 8ae8a564c2 Fix unit parsing in Tuya climate entities (#157964) 2025-12-05 20:31:56 +00:00
Jan Bouwhuis 2f72f57bb7 Move out zwave_js api docs url from strings.json (#157959) 2025-12-05 20:31:55 +00:00
Jan Bouwhuis e928e3cb54 Move Yeelight URLs out of translatable strings for action descriptions (#157957) 2025-12-05 20:31:53 +00:00
Petro31 b0e2109e15 Fix template migration errors (#157949) 2025-12-05 20:31:51 +00:00
Jordan Harvey b449c6673f Add pyanglianwater to Anglian Water loggers (#157947) 2025-12-05 20:31:50 +00:00
Manu 877ad38ac3 Convert image URLs to secure URLs in Xbox integration (#157945) 2025-12-05 20:31:48 +00:00
Jan Bouwhuis 229f45feae Move translatable URL from rainmachine push_weather_data action description (#157941)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-12-05 20:31:47 +00:00
Jordan Harvey a535d1f4eb Set account number as required for Anglian Water config entry (#157939) 2025-12-05 20:31:46 +00:00
Jan Bouwhuis d4adc00ae6 Move out URL of Xiaomy_aquara from strings.json (#157937)
Co-authored-by: Michelle "MishManners®™" Duke <36594527+mishmanners@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 20:31:44 +00:00
starkillerOG ba141f9d1d Bump reolink_aio to 0.17.1 (#157929) 2025-12-05 20:31:41 +00:00
cdnninja 72be9793a4 Fix VeSync binary sensor discovery (#157898) 2025-12-05 20:31:40 +00:00
Luke Lashley 5ae7cc5f84 Correctly pass MopParserConfig for Roborock (#157891) 2025-12-05 20:31:39 +00:00
Jan Bouwhuis d01a469b46 Move teslemetry time-of-use URL out of strings.json (#157874) 2025-12-05 20:31:37 +00:00
TheJulianJES 9f07052874 Display error when forming new ZHA network fails (#157863) 2025-12-05 20:31:35 +00:00
David Rapan b9bc9d3fc2 Fix Starlink's ever updating uptime (#155574)
Signed-off-by: David Rapan <david@rapan.cz>
2025-12-05 20:31:34 +00:00
Max Michels 1e180cd5ee Move telegram-bot URLs out of strings.json (#155130)
Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-12-05 20:31:32 +00:00
Quentin Ulmer dc9cdd13b1 Fix Rituals Perfume Genie (#151537)
Co-authored-by: Joostlek <joostlek@outlook.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-05 20:31:30 +00:00
2373 changed files with 109345 additions and 15138 deletions
+2 -1
View File
@@ -40,7 +40,8 @@
"python.terminal.activateEnvInCurrentTerminal": true,
"python.testing.pytestArgs": ["--no-cov"],
"pylint.importStrategy": "fromEnvironment",
"python.analysis.typeCheckingMode": "basic",
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
"python.analysis.typeCheckingMode": "off",
"editor.formatOnPaste": false,
"editor.formatOnSave": true,
"editor.formatOnType": true,
+3 -15
View File
@@ -847,8 +847,8 @@ rules:
## Development Commands
### Code Quality & Linting
- **Run all linters on all files**: `pre-commit run --all-files`
- **Run linters on staged files only**: `pre-commit run`
- **Run all linters on all files**: `prek run --all-files`
- **Run linters on staged files only**: `prek run`
- **PyLint on everything** (slow): `pylint homeassistant`
- **PyLint on specific folder**: `pylint homeassistant/components/my_integration`
- **MyPy type checking (whole project)**: `mypy homeassistant/`
@@ -1024,18 +1024,6 @@ class MyCoordinator(DataUpdateCoordinator[MyData]):
)
```
### Entity Performance Optimization
```python
# Use __slots__ for memory efficiency
class MySensor(SensorEntity):
__slots__ = ("_attr_native_value", "_attr_available")
@property
def should_poll(self) -> bool:
"""Disable polling when using coordinator."""
return False # ✅ Let coordinator handle updates
```
## Testing Patterns
### Testing Best Practices
@@ -1181,4 +1169,4 @@ python -m script.hassfest --integration-path homeassistant/components/my_integra
pytest ./tests/components/my_integration \
--cov=homeassistant.components.my_integration \
--cov-report term-missing
```
```
+5 -5
View File
@@ -100,7 +100,7 @@ jobs:
- name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend
@@ -111,7 +111,7 @@ jobs:
- name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@ac66b43f0e6a346234dd65d4d0c8fbb31cb316e5 # v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
github_token: ${{secrets.GITHUB_TOKEN}}
repo: OHF-Voice/intents-package
@@ -197,7 +197,7 @@ jobs:
cosign-release: "v2.5.3"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Build variables
id: vars
@@ -405,7 +405,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.7.1
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -551,7 +551,7 @@ jobs:
- name: Generate artifact attestation
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0
uses: actions/attest-build-provenance@00014ed6ed5efc5b1ab7f7f34a39eb55d41aa4f8 # v3.1.0
with:
subject-name: ${{ env.HASSFEST_IMAGE_NAME }}
subject-digest: ${{ steps.push.outputs.digest }}
+23 -158
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 2
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.1"
HA_SHORT_VERSION: "2026.2"
DEFAULT_PYTHON: "3.13.11"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']"
# 10.3 is the oldest supported version
@@ -59,7 +59,6 @@ env:
# 15 is the latest version
# - 15.2 is the latest (as of 9 Feb 2023)
POSTGRESQL_VERSIONS: "['postgres:12.14','postgres:15.2']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
UV_CACHE_DIR: /tmp/uv-cache
APT_CACHE_BASE: /home/runner/work/apt
APT_CACHE_DIR: /home/runner/work/apt/cache
@@ -83,7 +82,6 @@ jobs:
integrations_glob: ${{ steps.info.outputs.integrations_glob }}
integrations: ${{ steps.integrations.outputs.changes }}
apt_cache_key: ${{ steps.generate_apt_cache_key.outputs.key }}
pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }}
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
requirements: ${{ steps.core.outputs.requirements }}
mariadb_groups: ${{ steps.info.outputs.mariadb_groups }}
@@ -111,11 +109,6 @@ jobs:
hashFiles('requirements_all.txt') }}-${{
hashFiles('homeassistant/package_constraints.txt') }}-${{
hashFiles('script/gen_requirements_all.py') }}" >> $GITHUB_OUTPUT
- name: Generate partial pre-commit restore key
id: generate_pre-commit_cache_key
run: >-
echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{
hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Generate partial apt restore key
id: generate_apt_cache_key
run: |
@@ -244,8 +237,8 @@ jobs:
echo "skip_coverage: ${skip_coverage}"
echo "skip_coverage=${skip_coverage}" >> $GITHUB_OUTPUT
pre-commit:
name: Prepare pre-commit base
prek:
name: Run prek checks
runs-on: *runs-on-ubuntu
needs: [info]
if: |
@@ -254,147 +247,23 @@ jobs:
&& github.event.inputs.audit-licenses-only != 'true'
steps:
- *checkout
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-pre-commit-venv >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
run: |
python -m venv venv
. venv/bin/activate
python --version
pip install "$(grep '^uv' < requirements.txt)"
uv pip install "$(cat requirements_test.txt | grep pre-commit)"
- name: Restore pre-commit environment from cache
id: cache-precommit
uses: *actions-cache
with:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
key: &key-pre-commit-env >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true'
run: |
. venv/bin/activate
pre-commit install-hooks
lint-ruff-format:
name: Check ruff-format
runs-on: *runs-on-ubuntu
needs: &needs-pre-commit
- info
- pre-commit
steps:
- *checkout
- *setup-python-default
- &cache-restore-pre-commit-venv
name: Restore base Python virtual environment
id: cache-venv
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
fail-on-cache-miss: true
key: *key-pre-commit-venv
- &cache-restore-pre-commit-env
name: Restore pre-commit environment from cache
id: cache-precommit
uses: *actions-cache-restore
with:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: *key-pre-commit-env
- name: Run ruff-format
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff-format --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
lint-ruff:
name: Check ruff
runs-on: *runs-on-ubuntu
needs: *needs-pre-commit
steps:
- *checkout
- *setup-python-default
- *cache-restore-pre-commit-venv
- *cache-restore-pre-commit-env
- name: Run ruff
run: |
. venv/bin/activate
pre-commit run --hook-stage manual ruff-check --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
lint-other:
name: Check other linters
runs-on: *runs-on-ubuntu
needs: *needs-pre-commit
steps:
- *checkout
- *setup-python-default
- *cache-restore-pre-commit-venv
- *cache-restore-pre-commit-env
- name: Register yamllint problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/yamllint.json"
- name: Run yamllint
run: |
. venv/bin/activate
pre-commit run --hook-stage manual yamllint --all-files --show-diff-on-failure
- name: Register check-json problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-json.json"
- name: Run check-json
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-json --all-files --show-diff-on-failure
- name: Run prettier (fully)
if: needs.info.outputs.test_full_suite == 'true'
run: |
. venv/bin/activate
pre-commit run --hook-stage manual prettier --all-files --show-diff-on-failure
- name: Run prettier (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
shopt -s globstar
pre-commit run --hook-stage manual prettier --show-diff-on-failure --files {homeassistant,tests}/components/${{ needs.info.outputs.integrations_glob }}/{*,**/*}
- name: Register check executables problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
- name: Run executables check
run: |
. venv/bin/activate
pre-commit run --hook-stage manual check-executables-have-shebangs --all-files --show-diff-on-failure
- name: Register codespell problem matcher
run: |
echo "::add-matcher::.github/workflows/matchers/codespell.json"
- name: Run codespell
run: |
. venv/bin/activate
pre-commit run --show-diff-on-failure --hook-stage manual codespell --all-files
- name: Run prek
uses: j178/prek-action@9d6a3097e0c1865ecce00cfb89fe80f2ee91b547 # v1.0.12
env:
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config
RUFF_OUTPUT_FORMAT: github
lint-hadolint:
name: Check ${{ matrix.file }}
@@ -434,7 +303,7 @@ jobs:
- &setup-python-matrix
name: Set up Python ${{ matrix.python-version }}
id: python
uses: *actions-setup-python
uses: &actions-setup-python actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -447,7 +316,7 @@ jobs:
env.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
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: venv
key: &key-python-venv >-
@@ -562,7 +431,7 @@ jobs:
steps:
- &cache-restore-apt
name: Restore apt cache
uses: *actions-cache-restore
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: *path-apt-cache
fail-on-cache-miss: true
@@ -579,7 +448,13 @@ jobs:
-o Dir::State::Lists=${{ env.APT_LIST_CACHE_DIR }} \
libturbojpeg
- *checkout
- *setup-python-default
- &setup-python-default
name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: *actions-setup-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
- &cache-restore-python-default
name: Restore full Python ${{ env.DEFAULT_PYTHON }} virtual environment
id: cache-venv
@@ -782,9 +657,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
steps:
- *cache-restore-apt
@@ -823,9 +696,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
- prepare-pytest-full
if: |
@@ -949,9 +820,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'
@@ -1066,9 +935,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'
@@ -1202,9 +1069,7 @@ jobs:
- base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- prek
- mypy
if: |
needs.info.outputs.lint_only != 'true'
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize CodeQL
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
category: "/language:python"
+1
View File
@@ -92,6 +92,7 @@ pip-selfcheck.json
venv
.venv
Pipfile*
uv.lock
share/*
/Scripts/
+2 -2
View File
@@ -39,14 +39,14 @@ repos:
- id: prettier
additional_dependencies:
- prettier@3.6.2
- prettier-plugin-sort-json@4.1.1
- prettier-plugin-sort-json@4.2.0
- repo: https://github.com/cdce8p/python-typing-update
rev: v0.6.0
hooks:
# Run `python-typing-update` hook manually from time to time
# to update python typing syntax.
# Will require manual work, before submitting changes!
# pre-commit run --hook-stage manual python-typing-update --all-files
# prek run --hook-stage manual python-typing-update --all-files
- id: python-typing-update
stages: [manual]
args:
+2
View File
@@ -407,6 +407,7 @@ homeassistant.components.person.*
homeassistant.components.pi_hole.*
homeassistant.components.ping.*
homeassistant.components.plugwise.*
homeassistant.components.pooldose.*
homeassistant.components.portainer.*
homeassistant.components.powerfox.*
homeassistant.components.powerwall.*
@@ -567,6 +568,7 @@ homeassistant.components.wake_word.*
homeassistant.components.wallbox.*
homeassistant.components.waqi.*
homeassistant.components.water_heater.*
homeassistant.components.watts.*
homeassistant.components.watttime.*
homeassistant.components.weather.*
homeassistant.components.webhook.*
+2 -2
View File
@@ -7,8 +7,8 @@
"python.testing.pytestEnabled": false,
// https://code.visualstudio.com/docs/python/linting#_general-settings
"pylint.importStrategy": "fromEnvironment",
// Pyright is too pedantic for Home Assistant
"python.analysis.typeCheckingMode": "basic",
// Pyright type checking is not compatible with mypy which Home Assistant uses for type checking
"python.analysis.typeCheckingMode": "off",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
},
+4 -4
View File
@@ -45,7 +45,7 @@
{
"label": "Ruff",
"type": "shell",
"command": "pre-commit run ruff-check --all-files",
"command": "prek run ruff-check --all-files",
"group": {
"kind": "test",
"isDefault": true
@@ -57,9 +57,9 @@
"problemMatcher": []
},
{
"label": "Pre-commit",
"label": "Prek",
"type": "shell",
"command": "pre-commit run --show-diff-on-failure",
"command": "prek run --show-diff-on-failure",
"group": {
"kind": "test",
"isDefault": true
@@ -120,7 +120,7 @@
{
"label": "Generate Requirements",
"type": "shell",
"command": "./script/gen_requirements_all.py",
"command": "${command:python.interpreterPath} -m script.gen_requirements_all",
"group": {
"kind": "build",
"isDefault": true
Generated
+21 -6
View File
@@ -516,6 +516,8 @@ build.json @home-assistant/supervisor
/tests/components/fireservicerota/ @cyberjunky
/homeassistant/components/firmata/ @DaAwesomeP
/tests/components/firmata/ @DaAwesomeP
/homeassistant/components/fish_audio/ @noambav
/tests/components/fish_audio/ @noambav
/homeassistant/components/fitbit/ @allenporter
/tests/components/fitbit/ @allenporter
/homeassistant/components/fivem/ @Sander0542
@@ -530,6 +532,8 @@ build.json @home-assistant/supervisor
/tests/components/flo/ @dmulcahey
/homeassistant/components/flume/ @ChrisMandich @bdraco @jeeftor
/tests/components/flume/ @ChrisMandich @bdraco @jeeftor
/homeassistant/components/fluss/ @fluss
/tests/components/fluss/ @fluss
/homeassistant/components/flux_led/ @icemanch
/tests/components/flux_led/ @icemanch
/homeassistant/components/forecast_solar/ @klaasnicolaas @frenck
@@ -657,6 +661,8 @@ build.json @home-assistant/supervisor
/tests/components/harmony/ @ehendrix23 @bdraco @mkeesey @Aohzan
/homeassistant/components/hassio/ @home-assistant/supervisor
/tests/components/hassio/ @home-assistant/supervisor
/homeassistant/components/hdfury/ @glenndehaan
/tests/components/hdfury/ @glenndehaan
/homeassistant/components/hdmi_cec/ @inytar
/tests/components/hdmi_cec/ @inytar
/homeassistant/components/heatmiser/ @andylockran
@@ -664,8 +670,8 @@ build.json @home-assistant/supervisor
/tests/components/heos/ @andrewsayre
/homeassistant/components/here_travel_time/ @eifinger
/tests/components/here_travel_time/ @eifinger
/homeassistant/components/hikvision/ @mezz64
/tests/components/hikvision/ @mezz64
/homeassistant/components/hikvision/ @mezz64 @ptarjan
/tests/components/hikvision/ @mezz64 @ptarjan
/homeassistant/components/hikvisioncam/ @fbradyirl
/homeassistant/components/hisense_aehw4a1/ @bannhead
/tests/components/hisense_aehw4a1/ @bannhead
@@ -794,6 +800,8 @@ build.json @home-assistant/supervisor
/tests/components/intellifire/ @jeeftor
/homeassistant/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/intent/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/intent_script/ @arturpragacz
/tests/components/intent_script/ @arturpragacz
/homeassistant/components/intesishome/ @jnimmo
/homeassistant/components/iometer/ @jukrebs
/tests/components/iometer/ @jukrebs
@@ -1060,6 +1068,8 @@ build.json @home-assistant/supervisor
/tests/components/myuplink/ @pajzo @astrandb
/homeassistant/components/nam/ @bieniu
/tests/components/nam/ @bieniu
/homeassistant/components/namecheapdns/ @tr4nt0r
/tests/components/namecheapdns/ @tr4nt0r
/homeassistant/components/nanoleaf/ @milanmeu @joostlek
/tests/components/nanoleaf/ @milanmeu @joostlek
/homeassistant/components/nasweb/ @nasWebio
@@ -1164,6 +1174,8 @@ build.json @home-assistant/supervisor
/tests/components/open_router/ @joostlek
/homeassistant/components/openerz/ @misialq
/tests/components/openerz/ @misialq
/homeassistant/components/openevse/ @c00w @firstof9
/tests/components/openevse/ @c00w @firstof9
/homeassistant/components/openexchangerates/ @MartinHjelmare
/tests/components/openexchangerates/ @MartinHjelmare
/homeassistant/components/opengarage/ @danielhiversen
@@ -1195,8 +1207,8 @@ build.json @home-assistant/supervisor
/tests/components/ourgroceries/ @OnFreund
/homeassistant/components/overkiz/ @imicknl
/tests/components/overkiz/ @imicknl
/homeassistant/components/overseerr/ @joostlek
/tests/components/overseerr/ @joostlek
/homeassistant/components/overseerr/ @joostlek @AmGarera
/tests/components/overseerr/ @joostlek @AmGarera
/homeassistant/components/ovo_energy/ @timmo001
/tests/components/ovo_energy/ @timmo001
/homeassistant/components/p1_monitor/ @klaasnicolaas
@@ -1693,8 +1705,8 @@ build.json @home-assistant/supervisor
/tests/components/trafikverket_train/ @gjohansson-ST
/homeassistant/components/trafikverket_weatherstation/ @gjohansson-ST
/tests/components/trafikverket_weatherstation/ @gjohansson-ST
/homeassistant/components/transmission/ @engrbm87 @JPHutchins
/tests/components/transmission/ @engrbm87 @JPHutchins
/homeassistant/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/tests/components/transmission/ @engrbm87 @JPHutchins @andrew-codechimp
/homeassistant/components/trend/ @jpbede
/tests/components/trend/ @jpbede
/homeassistant/components/triggercmd/ @rvmey
@@ -1795,9 +1807,12 @@ build.json @home-assistant/supervisor
/tests/components/waqi/ @joostlek
/homeassistant/components/water_heater/ @home-assistant/core
/tests/components/water_heater/ @home-assistant/core
/homeassistant/components/waterfurnace/ @sdague @masterkoppa
/homeassistant/components/watergate/ @adam-the-hero
/tests/components/watergate/ @adam-the-hero
/homeassistant/components/watson_tts/ @rutkai
/homeassistant/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
/tests/components/watts/ @theobld-ww @devender-verma-ww @ssi-spyro
/homeassistant/components/watttime/ @bachya
/tests/components/watttime/ @bachya
/homeassistant/components/waze_travel_time/ @eifinger
Generated
+1 -1
View File
@@ -24,7 +24,7 @@ ENV \
COPY rootfs /
# Add go2rtc binary
COPY --from=ghcr.io/alexxit/go2rtc@sha256:baef0aa19d759fcfd31607b34ce8eaf039d496282bba57731e6ae326896d7640 /usr/local/bin/go2rtc /bin/go2rtc
COPY --from=ghcr.io/alexxit/go2rtc@sha256:f394f6329f5389a4c9a7fc54b09fdec9621bbb78bf7a672b973440bbdfb02241 /usr/local/bin/go2rtc /bin/go2rtc
RUN \
# Verify go2rtc can be executed
+2
View File
@@ -402,6 +402,8 @@ class AuthManager:
if user.is_owner:
raise ValueError("Unable to deactivate the owner")
await self._store.async_deactivate_user(user)
for refresh_token in list(user.refresh_tokens.values()):
self.async_remove_refresh_token(refresh_token)
async def async_remove_credentials(self, credentials: models.Credentials) -> None:
"""Remove credentials."""
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["accuweather"],
"requirements": ["accuweather==4.2.2"]
"requirements": ["accuweather==5.0.0"]
}
@@ -18,7 +18,7 @@ from .coordinator import (
ActronAirSystemCoordinator,
)
PLATFORM = [Platform.CLIMATE]
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
@@ -50,10 +50,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
system_coordinators=system_coordinators,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORM)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORM)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+14 -54
View File
@@ -15,12 +15,10 @@ from homeassistant.components.climate import (
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, ActronAirZoneEntity
PARALLEL_UPDATES = 0
@@ -56,8 +54,7 @@ async def async_setup_entry(
for coordinator in system_coordinators.values():
status = coordinator.data
name = status.ac_system.system_name
entities.append(ActronSystemClimate(coordinator, name))
entities.append(ActronSystemClimate(coordinator))
entities.extend(
ActronZoneClimate(coordinator, zone)
@@ -68,10 +65,9 @@ async def async_setup_entry(
async_add_entities(entities)
class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEntity):
class ActronAirClimateEntity(ClimateEntity):
"""Base class for Actron Air climate entities."""
_attr_has_entity_name = True
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
@@ -83,43 +79,17 @@ class BaseClimateEntity(CoordinatorEntity[ActronAirSystemCoordinator], ClimateEn
_attr_fan_modes = list(FAN_MODE_MAPPING_ACTRONAIR_TO_HA.values())
_attr_hvac_modes = list(HVAC_MODE_MAPPING_ACTRONAIR_TO_HA.values())
class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
"""Representation of the Actron Air system."""
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator)
self._serial_number = coordinator.serial_number
class ActronSystemClimate(BaseClimateEntity):
"""Representation of the Actron Air system."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_ON
| ClimateEntityFeature.TURN_OFF
)
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
name: str,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, name)
serial_number = coordinator.serial_number
self._attr_unique_id = serial_number
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=self._status.ac_system.system_name,
manufacturer="Actron Air",
model_id=self._status.ac_system.master_wc_model,
sw_version=self._status.ac_system.master_wc_firmware_version,
serial_number=serial_number,
)
self._attr_unique_id = self._serial_number
@property
def min_temp(self) -> float:
@@ -148,7 +118,7 @@ class ActronSystemClimate(BaseClimateEntity):
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_mode = self._status.user_aircon_settings.fan_mode
fan_mode = self._status.user_aircon_settings.base_fan_mode
return FAN_MODE_MAPPING_ACTRONAIR_TO_HA.get(fan_mode)
@property
@@ -168,7 +138,7 @@ class ActronSystemClimate(BaseClimateEntity):
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set a new fan mode."""
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode.lower())
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
@@ -182,7 +152,7 @@ class ActronSystemClimate(BaseClimateEntity):
await self._status.user_aircon_settings.set_temperature(temperature=temp)
class ActronZoneClimate(BaseClimateEntity):
class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
"""Representation of a zone within the Actron Air system."""
_attr_supported_features = (
@@ -197,18 +167,8 @@ class ActronZoneClimate(BaseClimateEntity):
zone: ActronAirZone,
) -> None:
"""Initialize an Actron Air unit."""
super().__init__(coordinator, zone.title)
serial_number = coordinator.serial_number
self._zone_id: int = zone.zone_id
self._attr_unique_id: str = f"{serial_number}_zone_{zone.zone_id}"
self._attr_device_info: DeviceInfo = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=zone.title,
manufacturer="Actron Air",
model="Zone",
suggested_area=zone.title,
via_device=(DOMAIN, serial_number),
)
super().__init__(coordinator, zone)
self._attr_unique_id: str = self._zone_identifier
@property
def min_temp(self) -> float:
@@ -256,4 +216,4 @@ class ActronZoneClimate(BaseClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
await self._zone.set_temperature(temperature=kwargs["temperature"])
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))
@@ -8,6 +8,7 @@ from datetime import timedelta
from actron_neo_api import (
ActronAirACSystem,
ActronAirAPI,
ActronAirAPIError,
ActronAirAuthError,
ActronAirStatus,
)
@@ -15,7 +16,7 @@ from actron_neo_api import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from .const import _LOGGER, DOMAIN
@@ -70,6 +71,12 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
translation_domain=DOMAIN,
translation_key="auth_error",
) from err
except ActronAirAPIError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={"error": repr(err)},
) from err
self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow()
@@ -0,0 +1,63 @@
"""Base entity classes for Actron Air integration."""
from actron_neo_api import ActronAirZone
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import ActronAirSystemCoordinator
class ActronAirEntity(CoordinatorEntity[ActronAirSystemCoordinator]):
"""Base class for Actron Air entities."""
_attr_has_entity_name = True
def __init__(self, coordinator: ActronAirSystemCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_number = coordinator.serial_number
@property
def available(self) -> bool:
"""Return True if entity is available."""
return not self.coordinator.is_device_stale()
class ActronAirAcEntity(ActronAirEntity):
"""Base class for Actron Air entities."""
def __init__(self, coordinator: ActronAirSystemCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._serial_number)},
name=coordinator.data.ac_system.system_name,
manufacturer="Actron Air",
model_id=coordinator.data.ac_system.master_wc_model,
sw_version=coordinator.data.ac_system.master_wc_firmware_version,
serial_number=self._serial_number,
)
class ActronAirZoneEntity(ActronAirEntity):
"""Base class for Actron Air zone entities."""
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
zone: ActronAirZone,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._zone_id: int = zone.zone_id
self._zone_identifier = f"{self._serial_number}_zone_{zone.zone_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._zone_identifier)},
name=zone.title,
manufacturer="Actron Air",
model="Zone",
suggested_area=zone.title,
via_device=(DOMAIN, self._serial_number),
)
@@ -0,0 +1,30 @@
{
"entity": {
"switch": {
"away_mode": {
"default": "mdi:home-export-outline",
"state": {
"off": "mdi:home-import-outline"
}
},
"continuous_fan": {
"default": "mdi:fan",
"state": {
"off": "mdi:fan-off"
}
},
"quiet_mode": {
"default": "mdi:volume-low",
"state": {
"off": "mdi:volume-high"
}
},
"turbo_mode": {
"default": "mdi:fan-plus",
"state": {
"off": "mdi:fan"
}
}
}
}
}
@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["actron-neo-api==0.2.0"]
"requirements": ["actron-neo-api==0.4.1"]
}
@@ -32,9 +32,28 @@
}
}
},
"entity": {
"switch": {
"away_mode": {
"name": "Away mode"
},
"continuous_fan": {
"name": "Continuous fan"
},
"quiet_mode": {
"name": "Quiet mode"
},
"turbo_mode": {
"name": "Turbo mode"
}
}
},
"exceptions": {
"auth_error": {
"message": "Authentication failed, please reauthenticate"
},
"update_error": {
"message": "An error occurred while retrieving data from the Actron Air API: {error}"
}
}
}
@@ -0,0 +1,102 @@
"""Switch platform for Actron Air integration."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ActronAirSwitchEntityDescription(SwitchEntityDescription):
"""Class describing Actron Air switch entities."""
is_on_fn: Callable[[ActronAirSystemCoordinator], bool]
set_fn: Callable[[ActronAirSystemCoordinator, bool], Awaitable[None]]
is_supported_fn: Callable[[ActronAirSystemCoordinator], bool] = lambda _: True
SWITCHES: tuple[ActronAirSwitchEntityDescription, ...] = (
ActronAirSwitchEntityDescription(
key="away_mode",
translation_key="away_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.away_mode,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_away_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="continuous_fan",
translation_key="continuous_fan",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.continuous_fan_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_continuous_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="quiet_mode",
translation_key="quiet_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.quiet_mode_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_quiet_mode(enabled),
),
ActronAirSwitchEntityDescription(
key="turbo_mode",
translation_key="turbo_mode",
is_on_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_enabled,
set_fn=lambda coordinator,
enabled: coordinator.data.user_aircon_settings.set_turbo_mode(enabled),
is_supported_fn=lambda coordinator: coordinator.data.user_aircon_settings.turbo_supported,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Actron Air switch entities."""
system_coordinators = entry.runtime_data.system_coordinators
async_add_entities(
ActronAirSwitch(coordinator, description)
for coordinator in system_coordinators.values()
for description in SWITCHES
if description.is_supported_fn(coordinator)
)
class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
"""Actron Air switch."""
_attr_entity_category = EntityCategory.CONFIG
entity_description: ActronAirSwitchEntityDescription
def __init__(
self,
coordinator: ActronAirSystemCoordinator,
description: ActronAirSwitchEntityDescription,
) -> None:
"""Initialize the switch."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
@property
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_fn(self.coordinator, True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_fn(self.coordinator, False)
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
"loggers": ["adax", "adax_local"],
"requirements": ["adax==0.4.0", "Adax-local==0.2.0"]
"requirements": ["adax==0.4.0", "Adax-local==0.3.0"]
}
+6 -1
View File
@@ -7,7 +7,12 @@ from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BUTTON,
Platform.CLIMATE,
Platform.NUMBER,
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:
@@ -0,0 +1,96 @@
"""Button platform for Airobot integration."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from pyairobotrest.exceptions import (
AirobotConnectionError,
AirobotError,
AirobotTimeoutError,
)
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotButtonEntityDescription(ButtonEntityDescription):
"""Describes Airobot button entity."""
press_fn: Callable[[AirobotDataUpdateCoordinator], Coroutine[Any, Any, None]]
BUTTON_TYPES: tuple[AirobotButtonEntityDescription, ...] = (
AirobotButtonEntityDescription(
key="restart",
device_class=ButtonDeviceClass.RESTART,
entity_category=EntityCategory.CONFIG,
press_fn=lambda coordinator: coordinator.client.reboot_thermostat(),
),
AirobotButtonEntityDescription(
key="recalibrate_co2",
translation_key="recalibrate_co2",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
press_fn=lambda coordinator: coordinator.client.recalibrate_co2_sensor(),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot button entities."""
coordinator = entry.runtime_data
async_add_entities(
AirobotButton(coordinator, description) for description in BUTTON_TYPES
)
class AirobotButton(AirobotEntity, ButtonEntity):
"""Representation of an Airobot button."""
entity_description: AirobotButtonEntityDescription
def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
description: AirobotButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
async def async_press(self) -> None:
"""Handle the button press."""
try:
await self.entity_description.press_fn(self.coordinator)
except (AirobotConnectionError, AirobotTimeoutError):
# Connection errors during reboot are expected as device restarts
pass
except AirobotError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="button_press_failed",
translation_placeholders={"button": self.entity_description.key},
) from err
@@ -175,6 +175,42 @@ class AirobotConfigFlow(BaseConfigFlow, domain=DOMAIN):
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of the integration."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
# Verify the device ID matches the existing config entry
await self.async_set_unique_id(info.device_id)
self._abort_if_unique_id_mismatch(reason="wrong_device")
return self.async_update_reload_and_abort(
reconfigure_entry,
data_updates=user_input,
title=info.title,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, reconfigure_entry.data
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
@@ -0,0 +1,14 @@
{
"entity": {
"button": {
"recalibrate_co2": {
"default": "mdi:molecule-co2"
}
},
"number": {
"hysteresis_band": {
"default": "mdi:delta"
}
}
}
}
@@ -12,6 +12,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pyairobotrest"],
"quality_scale": "silver",
"requirements": ["pyairobotrest==0.1.0"]
"quality_scale": "gold",
"requirements": ["pyairobotrest==0.2.0"]
}
@@ -0,0 +1,99 @@
"""Number platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from pyairobotrest.const import HYSTERESIS_BAND_MAX, HYSTERESIS_BAND_MIN
from pyairobotrest.exceptions import AirobotError
from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import EntityCategory, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirobotConfigEntry
from .const import DOMAIN
from .coordinator import AirobotDataUpdateCoordinator
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotNumberEntityDescription(NumberEntityDescription):
"""Describes Airobot number entity."""
value_fn: Callable[[AirobotDataUpdateCoordinator], float]
set_value_fn: Callable[[AirobotDataUpdateCoordinator, float], Awaitable[None]]
NUMBERS: tuple[AirobotNumberEntityDescription, ...] = (
AirobotNumberEntityDescription(
key="hysteresis_band",
translation_key="hysteresis_band",
device_class=NumberDeviceClass.TEMPERATURE,
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
native_min_value=HYSTERESIS_BAND_MIN / 10.0,
native_max_value=HYSTERESIS_BAND_MAX / 10.0,
native_step=0.1,
value_fn=lambda coordinator: coordinator.data.settings.hysteresis_band,
set_value_fn=lambda coordinator, value: coordinator.client.set_hysteresis_band(
value
),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot number platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotNumber(coordinator, description) for description in NUMBERS
)
class AirobotNumber(AirobotEntity, NumberEntity):
"""Representation of an Airobot number entity."""
entity_description: AirobotNumberEntityDescription
def __init__(
self,
coordinator: AirobotDataUpdateCoordinator,
description: AirobotNumberEntityDescription,
) -> None:
"""Initialize the number entity."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> float:
"""Return the current value."""
return self.entity_description.value_fn(self.coordinator)
async def async_set_native_value(self, value: float) -> None:
"""Set the value."""
try:
await self.entity_description.set_value_fn(self.coordinator, value)
except AirobotError as err:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="set_value_failed",
translation_placeholders={"error": str(err)},
) from err
else:
await self.coordinator.async_request_refresh()
@@ -43,12 +43,12 @@ rules:
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: todo
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: todo
docs-use-cases: done
dynamic-devices:
status: exempt
comment: Single device integration, no dynamic device discovery needed.
@@ -57,8 +57,8 @@ rules:
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: todo
reconfiguration-flow: todo
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
comment: This integration doesn't have any cases where raising an issue is needed.
+32 -1
View File
@@ -2,7 +2,9 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"wrong_device": "Device ID does not match the existing configuration. Please use the correct device credentials."
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -28,6 +30,19 @@
},
"description": "The authentication for Airobot thermostat at {host} (Device ID: {username}) has expired. Please enter the password to reauthenticate. Find the password in the thermostat settings menu under Connectivity → Mobile app."
},
"reconfigure": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "Device ID"
},
"data_description": {
"host": "[%key:component::airobot::config::step::user::data_description::host%]",
"password": "[%key:component::airobot::config::step::user::data_description::password%]",
"username": "[%key:component::airobot::config::step::user::data_description::username%]"
},
"description": "Update your Airobot thermostat connection details. Note: The Device ID must remain the same as the original configuration."
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
@@ -44,6 +59,16 @@
}
},
"entity": {
"button": {
"recalibrate_co2": {
"name": "Recalibrate CO2 sensor"
}
},
"number": {
"hysteresis_band": {
"name": "Hysteresis band"
}
},
"sensor": {
"air_temperature": {
"name": "Air temperature"
@@ -66,6 +91,9 @@
"authentication_failed": {
"message": "Authentication failed, please reauthenticate."
},
"button_press_failed": {
"message": "Failed to press {button} button."
},
"connection_failed": {
"message": "Failed to communicate with device."
},
@@ -74,6 +102,9 @@
},
"set_temperature_failed": {
"message": "Failed to set temperature to {temperature}."
},
"set_value_failed": {
"message": "Failed to set value: {error}"
}
}
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["airos==0.6.0"]
"requirements": ["airos==0.6.1"]
}
@@ -88,21 +88,11 @@ class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
super().__init__(coordinator, unit_id)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data."""
return self.device_data.get("climate") or {}
@property
def params(self) -> dict[str, Any]:
"""Return the current parameters for the climate entity."""
return self.climate_data.get("ParametersData") or {}
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and bool(self.climate_data)
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
+1 -1
View File
@@ -10,7 +10,7 @@ from homeassistant.const import Platform
DOMAIN = "airpatrol"
LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.CLIMATE]
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR]
SCAN_INTERVAL = timedelta(minutes=1)
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)
+11 -1
View File
@@ -38,7 +38,17 @@ class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
"""Return the device data."""
return self.coordinator.data[self._unit_id]
@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data for this unit."""
return self.device_data["climate"]
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._unit_id in self.coordinator.data
return (
super().available
and self._unit_id in self.coordinator.data
and "climate" in self.device_data
and self.climate_data is not None
)
@@ -0,0 +1,89 @@
"""Sensors for AirPatrol integration."""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirPatrolConfigEntry
from .coordinator import AirPatrolDataUpdateCoordinator
from .entity import AirPatrolEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirPatrolSensorEntityDescription(SensorEntityDescription):
"""Describes AirPatrol sensor entity."""
data_field: str
SENSOR_DESCRIPTIONS = (
AirPatrolSensorEntityDescription(
key="temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
data_field="RoomTemp",
),
AirPatrolSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
data_field="RoomHumidity",
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirPatrolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AirPatrol sensors."""
coordinator = config_entry.runtime_data
units = coordinator.data
async_add_entities(
AirPatrolSensor(coordinator, unit_id, description)
for unit_id, unit in units.items()
for description in SENSOR_DESCRIPTIONS
if "climate" in unit and unit["climate"] is not None
)
class AirPatrolSensor(AirPatrolEntity, SensorEntity):
"""AirPatrol sensor entity."""
entity_description: AirPatrolSensorEntityDescription
def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
description: AirPatrolSensorEntityDescription,
) -> None:
"""Initialize AirPatrol sensor."""
super().__init__(coordinator, unit_id)
self.entity_description = description
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id}-{unit_id}-{description.key}"
)
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
if value := self.climate_data.get(self.entity_description.data_field):
return float(value)
return None
@@ -85,6 +85,22 @@ class AirzoneSystemEntity(AirzoneEntity):
value = system[key]
return value
async def _async_update_sys_params(self, params: dict[str, Any]) -> None:
"""Send system parameters to API."""
_params = {
API_SYSTEM_ID: self.system_id,
**params,
}
_LOGGER.debug("update_sys_params=%s", _params)
try:
await self.coordinator.airzone.set_sys_parameters(_params)
except AirzoneError as error:
raise HomeAssistantError(
f"Failed to set system {self.entity_id}: {error}"
) from error
self.coordinator.async_set_updated_data(self.coordinator.airzone.data())
class AirzoneHotWaterEntity(AirzoneEntity):
"""Define an Airzone Hot Water entity."""
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.4"]
"requirements": ["aioairzone==1.0.5"]
}
+73 -14
View File
@@ -20,6 +20,7 @@ from aioairzone.const import (
AZD_MODES,
AZD_Q_ADAPT,
AZD_SLEEP,
AZD_SYSTEMS,
AZD_ZONES,
)
@@ -30,7 +31,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import AirzoneConfigEntry, AirzoneUpdateCoordinator
from .entity import AirzoneEntity, AirzoneZoneEntity
from .entity import AirzoneEntity, AirzoneSystemEntity, AirzoneZoneEntity
@dataclass(frozen=True, kw_only=True)
@@ -85,14 +86,7 @@ def main_zone_options(
return [k for k, v in options.items() if v in modes]
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_MODE,
key=AZD_MODE,
options_dict=MODE_DICT,
options_fn=main_zone_options,
translation_key="modes",
),
SYSTEM_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_Q_ADAPT,
entity_category=EntityCategory.CONFIG,
@@ -104,6 +98,17 @@ MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
)
MAIN_ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_MODE,
key=AZD_MODE,
options_dict=MODE_DICT,
options_fn=main_zone_options,
translation_key="modes",
),
)
ZONE_SELECT_TYPES: Final[tuple[AirzoneSelectDescription, ...]] = (
AirzoneSelectDescription(
api_param=API_COLD_ANGLE,
@@ -140,16 +145,37 @@ async def async_setup_entry(
"""Add Airzone select from a config_entry."""
coordinator = entry.runtime_data
added_systems: set[str] = set()
added_zones: set[str] = set()
def _async_entity_listener() -> None:
"""Handle additions of select."""
entities: list[AirzoneBaseSelect] = []
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
received_systems = set(systems_data)
new_systems = received_systems - added_systems
if new_systems:
entities.extend(
AirzoneSystemSelect(
coordinator,
description,
entry,
system_id,
systems_data.get(system_id),
)
for system_id in new_systems
for description in SYSTEM_SELECT_TYPES
if description.key in systems_data.get(system_id)
)
added_systems.update(new_systems)
zones_data = coordinator.data.get(AZD_ZONES, {})
received_zones = set(zones_data)
new_zones = received_zones - added_zones
if new_zones:
entities: list[AirzoneZoneSelect] = [
entities.extend(
AirzoneZoneSelect(
coordinator,
description,
@@ -161,8 +187,8 @@ async def async_setup_entry(
for description in MAIN_ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
and zones_data.get(system_zone_id).get(AZD_MASTER) is True
]
entities += [
)
entities.extend(
AirzoneZoneSelect(
coordinator,
description,
@@ -173,10 +199,11 @@ async def async_setup_entry(
for system_zone_id in new_zones
for description in ZONE_SELECT_TYPES
if description.key in zones_data.get(system_zone_id)
]
async_add_entities(entities)
)
added_zones.update(new_zones)
async_add_entities(entities)
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
_async_entity_listener()
@@ -203,6 +230,38 @@ class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
self._attr_current_option = self._get_current_option()
class AirzoneSystemSelect(AirzoneSystemEntity, AirzoneBaseSelect):
"""Define an Airzone System select."""
def __init__(
self,
coordinator: AirzoneUpdateCoordinator,
description: AirzoneSelectDescription,
entry: ConfigEntry,
system_id: str,
system_data: dict[str, Any],
) -> None:
"""Initialize."""
super().__init__(coordinator, entry, system_data)
self._attr_unique_id = f"{self._attr_unique_id}_{system_id}_{description.key}"
self.entity_description = description
self._attr_options = self.entity_description.options_fn(
system_data, description.options_dict
)
self.values_dict = {v: k for k, v in description.options_dict.items()}
self._async_update_attrs()
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
param = self.entity_description.api_param
value = self.entity_description.options_dict[option]
await self._async_update_sys_params({param: value})
class AirzoneZoneSelect(AirzoneZoneEntity, AirzoneBaseSelect):
"""Define an Airzone Zone select."""
@@ -45,7 +45,7 @@ def make_entity_state_trigger_required_features(
"""Trigger for entity state changes."""
_domain = domain
_to_state = to_state
_to_states = {to_state}
_required_features = required_features
return CustomTrigger
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==10.0.0"]
"requirements": ["aioamazondevices==11.0.2"]
}
@@ -3,7 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
from aiohttp import CookieJar
from pyanglianwater import AnglianWater
@@ -30,14 +30,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
)
async def validate_credentials(
auth: MSOB2CAuth, account_number: str
) -> str | MSOB2CAuth:
async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
"""Validate the provided credentials."""
try:
await auth.send_login_request()
@@ -46,6 +43,33 @@ async def validate_credentials(
except Exception:
_LOGGER.exception("Unexpected exception")
return "unknown"
return auth
def humanize_account_data(account: dict) -> str:
"""Convert an account data into a human-readable format."""
if account["address"]["company_name"] != "":
return f"{account['account_number']} - {account['address']['company_name']}"
if account["address"]["building_name"] != "":
return f"{account['account_number']} - {account['address']['building_name']}"
return f"{account['account_number']} - {account['address']['postcode']}"
async def get_accounts(auth: MSOB2CAuth) -> list[selector.SelectOptionDict]:
"""Retrieve the list of accounts associated with the authenticated user."""
_aw = AnglianWater(authenticator=auth)
accounts = await _aw.api.get_associated_accounts()
return [
selector.SelectOptionDict(
value=str(account["account_number"]),
label=humanize_account_data(account),
)
for account in accounts["result"]["active"]
]
async def validate_account(auth: MSOB2CAuth, account_number: str) -> str | MSOB2CAuth:
"""Validate the provided account number."""
_aw = AnglianWater(authenticator=auth)
try:
await _aw.validate_smart_meter(account_number)
@@ -57,36 +81,91 @@ async def validate_credentials(
class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anglian Water."""
def __init__(self) -> None:
"""Initialize the config flow."""
self.authenticator: MSOB2CAuth | None = None
self.accounts: list[selector.SelectOptionDict] = []
self.user_input: dict[str, Any] | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
validation_response = await validate_credentials(
MSOB2CAuth(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
session=async_create_clientsession(
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
self.authenticator = MSOB2CAuth(
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
session=async_create_clientsession(
self.hass,
cookie_jar=CookieJar(quote_cookie=False),
),
user_input[CONF_ACCOUNT_NUMBER],
)
validation_response = await validate_credentials(self.authenticator)
if isinstance(validation_response, str):
errors["base"] = validation_response
else:
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
data={
**user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token,
},
self.accounts = await get_accounts(self.authenticator)
if len(self.accounts) > 1:
self.user_input = user_input
return await self.async_step_select_account()
account_number = self.accounts[0]["value"]
self.user_input = user_input
return await self.async_step_complete(
{
CONF_ACCOUNT_NUMBER: account_number,
}
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_select_account(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the account selection step."""
errors = {}
if user_input is not None:
if TYPE_CHECKING:
assert self.authenticator
validation_result = await validate_account(
self.authenticator,
user_input[CONF_ACCOUNT_NUMBER],
)
if isinstance(validation_result, str):
errors["base"] = validation_result
else:
return await self.async_step_complete(user_input)
return self.async_show_form(
step_id="select_account",
data_schema=vol.Schema(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.SelectSelector(
selector.SelectSelectorConfig(
options=self.accounts,
multiple=False,
mode=selector.SelectSelectorMode.DROPDOWN,
)
)
}
),
errors=errors,
)
async def async_step_complete(self, user_input: dict[str, Any]) -> ConfigFlowResult:
"""Handle the final configuration step."""
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER])
self._abort_if_unique_id_configured()
if TYPE_CHECKING:
assert self.authenticator
assert self.user_input
config_entry_data = {
**self.user_input,
CONF_ACCOUNT_NUMBER: user_input[CONF_ACCOUNT_NUMBER],
CONF_ACCESS_TOKEN: self.authenticator.refresh_token,
}
return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER],
data=config_entry_data,
)
@@ -4,13 +4,28 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from pyanglianwater import AnglianWater
from pyanglianwater.exceptions import ExpiredAccessTokenError, UnknownEndpointError
from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import (
StatisticData,
StatisticMeanType,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import VolumeConverter
from .const import CONF_ACCOUNT_NUMBER, DOMAIN
@@ -44,6 +59,107 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None:
"""Update data from Anglian Water's API."""
try:
return await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER])
await self._insert_statistics()
except (ExpiredAccessTokenError, UnknownEndpointError) as err:
raise UpdateFailed from err
async def _insert_statistics(self) -> None:
"""Insert statistics for water meters into Home Assistant."""
for meter in self.api.meters.values():
id_prefix = (
f"{self.config_entry.data[CONF_ACCOUNT_NUMBER]}_{meter.serial_number}"
)
usage_statistic_id = f"{DOMAIN}:{id_prefix}_usage".lower()
_LOGGER.debug("Updating statistics for meter %s", meter.serial_number)
name_prefix = (
f"Anglian Water {self.config_entry.data[CONF_ACCOUNT_NUMBER]} "
f"{meter.serial_number}"
)
usage_metadata = StatisticMetaData(
mean_type=StatisticMeanType.NONE,
has_sum=True,
name=f"{name_prefix} Usage",
source=DOMAIN,
statistic_id=usage_statistic_id,
unit_class=VolumeConverter.UNIT_CLASS,
unit_of_measurement=UnitOfVolume.CUBIC_METERS,
)
last_stat = await get_instance(self.hass).async_add_executor_job(
get_last_statistics, self.hass, 1, usage_statistic_id, True, set()
)
if not last_stat:
_LOGGER.debug("Updating statistics for the first time")
usage_sum = 0.0
last_stats_time = None
else:
if not meter.readings or len(meter.readings) == 0:
_LOGGER.debug("No recent usage statistics found, skipping update")
continue
# Anglian Water stats are hourly, the read_at time is the time that the meter took the reading
# We remove 1 hour from this so that the data is shown in the correct hour on the dashboards
parsed_read_at = dt_util.parse_datetime(meter.readings[0]["read_at"])
if not parsed_read_at:
_LOGGER.debug(
"Could not parse read_at time %s, skipping update",
meter.readings[0]["read_at"],
)
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
_LOGGER.debug("Getting statistics at %s", start)
for end in (start + timedelta(seconds=1), None):
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
self.hass,
start,
end,
{
usage_statistic_id,
},
"hour",
None,
{"sum"},
)
if stats:
break
if end:
_LOGGER.debug(
"Not found, trying to find oldest statistic after %s",
start,
)
assert stats
def _safe_get_sum(records: list[Any]) -> float:
if records and "sum" in records[0]:
return float(records[0]["sum"])
return 0.0
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
last_stats_time = stats[usage_statistic_id][0]["start"]
usage_statistics = []
for read in meter.readings:
parsed_read_at = dt_util.parse_datetime(read["read_at"])
if not parsed_read_at:
_LOGGER.debug(
"Could not parse read_at time %s, skipping reading",
read["read_at"],
)
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
if last_stats_time is not None and start.timestamp() <= last_stats_time:
continue
usage_state = max(0, read["consumption"] / 1000)
usage_sum = max(0, read["read"])
usage_statistics.append(
StatisticData(
start=start,
state=usage_state,
sum=usage_sum,
)
)
_LOGGER.debug(
"Adding %s statistics for %s", len(usage_statistics), usage_statistic_id
)
async_add_external_statistics(self.hass, usage_metadata, usage_statistics)
@@ -1,6 +1,7 @@
{
"domain": "anglian_water",
"name": "Anglian Water",
"after_dependencies": ["recorder"],
"codeowners": ["@pantherale0"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water",
@@ -10,14 +10,21 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"select_account": {
"data": {
"account_number": "Billing account number"
},
"data_description": {
"account_number": "Select the billing account you wish to use."
},
"description": "Multiple active billing accounts were found with your credentials. Please select the account you wish to use. If this is unexpected, contact Anglian Water to confirm your active accounts."
},
"user": {
"data": {
"account_number": "Billing Account Number",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"account_number": "Your account number found on your latest bill.",
"password": "Your password",
"username": "Username or email used to log in to the Anglian Water website."
},
+2 -1
View File
@@ -69,6 +69,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, llm
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.json import json_dumps
from homeassistant.util import slugify
from . import AnthropicConfigEntry
@@ -193,7 +194,7 @@ def _convert_content(
tool_result_block = ToolResultBlockParam(
type="tool_result",
tool_use_id=content.tool_call_id,
content=json.dumps(content.tool_result),
content=json_dumps(content.tool_result),
)
external_tool = False
if not messages or messages[-1]["role"] != (
+15 -1
View File
@@ -4,6 +4,8 @@ from __future__ import annotations
import logging
import dateutil
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.script import scripts_with_entity
from homeassistant.components.sensor import (
@@ -179,6 +181,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
LAST_S_TEST: SensorEntityDescription(
key=LAST_S_TEST,
translation_key="last_self_test",
device_class=SensorDeviceClass.TIMESTAMP,
),
"lastxfer": SensorEntityDescription(
key="lastxfer",
@@ -232,6 +235,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
"masterupd": SensorEntityDescription(
key="masterupd",
translation_key="master_update",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"maxlinev": SensorEntityDescription(
@@ -365,6 +369,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
"starttime": SensorEntityDescription(
key="starttime",
translation_key="startup_time",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"statflag": SensorEntityDescription(
@@ -416,16 +421,19 @@ SENSORS: dict[str, SensorEntityDescription] = {
"xoffbat": SensorEntityDescription(
key="xoffbat",
translation_key="transfer_from_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xoffbatt": SensorEntityDescription(
key="xoffbatt",
translation_key="transfer_from_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
"xonbatt": SensorEntityDescription(
key="xonbatt",
translation_key="transfer_to_battery",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
),
}
@@ -529,7 +537,13 @@ class APCUPSdSensor(APCUPSdEntity, SensorEntity):
self._attr_native_value = None
return
self._attr_native_value, inferred_unit = infer_unit(self.coordinator.data[key])
data = self.coordinator.data[key]
if self.entity_description.device_class == SensorDeviceClass.TIMESTAMP:
self._attr_native_value = dateutil.parser.parse(data)
return
self._attr_native_value, inferred_unit = infer_unit(data)
if not self.native_unit_of_measurement:
self._attr_native_unit_of_measurement = inferred_unit
@@ -3,9 +3,8 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
import logging
import math
from pysilero_vad import SileroVoiceActivityDetector
from pymicro_vad import MicroVad
from pyspeex_noise import AudioProcessor
from .const import BYTES_PER_CHUNK
@@ -43,8 +42,8 @@ class AudioEnhancer(ABC):
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
class SileroVadSpeexEnhancer(AudioEnhancer):
"""Audio enhancer that runs Silero VAD and speex."""
class MicroVadSpeexEnhancer(AudioEnhancer):
"""Audio enhancer that runs microVAD and speex."""
def __init__(
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
@@ -70,49 +69,21 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
self.noise_suppression,
)
self.vad: SileroVoiceActivityDetector | None = None
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
# buffer audio. The previous speech probability is used until enough
# audio has been buffered.
self._vad_buffer: bytearray | None = None
self._vad_buffer_chunks = 0
self._vad_buffer_chunk_idx = 0
self._last_speech_probability: float | None = None
self.vad: MicroVad | None = None
if self.is_vad_enabled:
self.vad = SileroVoiceActivityDetector()
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
self._vad_buffer_chunks = int(
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
)
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
self._vad_buffer = bytearray(self.vad.chunk_bytes())
_LOGGER.debug("Initialized Silero VAD")
self.vad = MicroVad()
_LOGGER.debug("Initialized microVAD")
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
speech_probability: float | None = None
assert len(audio) == BYTES_PER_CHUNK
if self.vad is not None:
# Run VAD
assert self._vad_buffer is not None
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
self._vad_buffer_chunk_idx += 1
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
# We have enough data to run Silero VAD (32 ms)
self._last_speech_probability = self.vad.process_chunk(
self._vad_buffer[: self.vad.chunk_bytes()]
)
# Copy leftover audio that wasn't processed to start
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
-self._vad_leftover_bytes :
]
self._vad_buffer_chunk_idx = 0
speech_probability = self.vad.Process10ms(audio)
if self.audio_processor is not None:
# Run noise suppression and auto gain
@@ -121,5 +92,5 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
return EnhancedAudioChunk(
audio=audio,
timestamp_ms=timestamp_ms,
speech_probability=self._last_speech_probability,
speech_probability=speech_probability,
)
@@ -8,5 +8,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"]
"requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
}
@@ -55,7 +55,7 @@ from homeassistant.util import (
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.limited_size_dict import LimitedSizeDict
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
from .const import (
ACKNOWLEDGE_PATH,
BYTES_PER_CHUNK,
@@ -633,7 +633,7 @@ class PipelineRun:
# Initialize with audio settings
if self.audio_settings.needs_processor and (self.audio_enhancer is None):
# Default audio enhancer
self.audio_enhancer = SileroVadSpeexEnhancer(
self.audio_enhancer = MicroVadSpeexEnhancer(
self.audio_settings.auto_gain_dbfs,
self.audio_settings.noise_suppression_level,
self.audio_settings.is_vad_enabled,
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
}
@@ -7,7 +7,7 @@ import asyncio
from collections.abc import Callable, Mapping
from dataclasses import dataclass
import logging
from typing import Any, Protocol, cast
from typing import Any, Literal, Protocol, cast
from propcache.api import cached_property
import voluptuous as vol
@@ -16,7 +16,10 @@ from homeassistant.components import labs, websocket_api
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.components.labs import async_listen as async_labs_listen
from homeassistant.const import (
ATTR_AREA_ID,
ATTR_ENTITY_ID,
ATTR_FLOOR_ID,
ATTR_LABEL_ID,
ATTR_MODE,
ATTR_NAME,
CONF_ACTIONS,
@@ -27,8 +30,10 @@ from homeassistant.const import (
CONF_EVENT_DATA,
CONF_ID,
CONF_MODE,
CONF_OPTIONS,
CONF_PATH,
CONF_PLATFORM,
CONF_TARGET,
CONF_TRIGGERS,
CONF_VARIABLES,
CONF_ZONE,
@@ -118,6 +123,7 @@ SERVICE_TRIGGER = "trigger"
NEW_TRIGGERS_CONDITIONS_FEATURE_FLAG = "new_triggers_conditions"
_EXPERIMENTAL_CONDITION_PLATFORMS = {
"fan",
"light",
}
@@ -130,9 +136,14 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"cover",
"device_tracker",
"fan",
"humidifier",
"lawn_mower",
"light",
"lock",
"media_player",
"person",
"scene",
"siren",
"switch",
"text",
"update",
@@ -583,20 +594,32 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity):
"""Return True if entity is on."""
return self._async_detach_triggers is not None or self._is_enabled
@property
@cached_property
def referenced_labels(self) -> set[str]:
"""Return a set of referenced labels."""
return self.action_script.referenced_labels
referenced = self.action_script.referenced_labels
@property
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_LABEL_ID))
return referenced
@cached_property
def referenced_floors(self) -> set[str]:
"""Return a set of referenced floors."""
return self.action_script.referenced_floors
referenced = self.action_script.referenced_floors
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_FLOOR_ID))
return referenced
@cached_property
def referenced_areas(self) -> set[str]:
"""Return a set of referenced areas."""
return self.action_script.referenced_areas
referenced = self.action_script.referenced_areas
for conf in self._trigger_config:
referenced |= set(_get_targets_from_trigger_config(conf, ATTR_AREA_ID))
return referenced
@property
def referenced_blueprint(self) -> str | None:
@@ -1204,6 +1227,9 @@ def _trigger_extract_devices(trigger_conf: dict) -> list[str]:
if trigger_conf[CONF_PLATFORM] == "tag" and CONF_DEVICE_ID in trigger_conf:
return trigger_conf[CONF_DEVICE_ID] # type: ignore[no-any-return]
if target_devices := _get_targets_from_trigger_config(trigger_conf, CONF_DEVICE_ID):
return target_devices
return []
@@ -1214,7 +1240,7 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
return trigger_conf[CONF_ENTITY_ID] # type: ignore[no-any-return]
if trigger_conf[CONF_PLATFORM] == "calendar":
return [trigger_conf[CONF_ENTITY_ID]]
return [trigger_conf[CONF_OPTIONS][CONF_ENTITY_ID]]
if trigger_conf[CONF_PLATFORM] == "zone":
return trigger_conf[CONF_ENTITY_ID] + [trigger_conf[CONF_ZONE]] # type: ignore[no-any-return]
@@ -1234,9 +1260,28 @@ def _trigger_extract_entities(trigger_conf: dict) -> list[str]:
):
return [trigger_conf[CONF_EVENT_DATA][CONF_ENTITY_ID]]
if target_entities := _get_targets_from_trigger_config(
trigger_conf, CONF_ENTITY_ID
):
return target_entities
return []
@callback
def _get_targets_from_trigger_config(
config: dict,
target: Literal["entity_id", "device_id", "area_id", "floor_id", "label_id"],
) -> list[str]:
"""Extract targets from a target config."""
if not (target_conf := config.get(CONF_TARGET)):
return []
if not (targets := target_conf.get(target)):
return []
return [targets] if isinstance(targets, str) else targets
@websocket_api.websocket_command({"type": "automation/config", "entity_id": str})
def websocket_config(
hass: HomeAssistant,
+1 -1
View File
@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==65"],
"requirements": ["axis==66"],
"ssdp": [
{
"manufacturer": "AXIS"
@@ -80,7 +80,7 @@ class AzureDataExplorerClient:
def test_connection(self) -> None:
"""Test connection, will throw Exception if it cannot connect."""
query = f"{self._table} | take 1"
query = f"['{self._table}'] | take 1"
self.query_client.execute_query(self._database, query)
@@ -45,7 +45,7 @@ class ADXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
async def validate_input(self, data: dict[str, Any]) -> dict[str, Any] | None:
async def validate_input(self, data: dict[str, Any]) -> dict[str, str]:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
@@ -54,36 +54,40 @@ class ADXConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
await self.hass.async_add_executor_job(client.test_connection)
except KustoAuthenticationError as exp:
_LOGGER.error(exp)
except KustoAuthenticationError as err:
_LOGGER.error("Authentication failed: %s", err)
return {"base": "invalid_auth"}
except KustoServiceError as exp:
_LOGGER.error(exp)
except KustoServiceError as err:
_LOGGER.error("Could not connect: %s", err)
return {"base": "cannot_connect"}
return None
return {}
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict = {}
if user_input:
errors = await self.validate_input(user_input) # type: ignore[assignment]
errors: dict[str, str] = {}
data_schema = STEP_USER_DATA_SCHEMA
if user_input is not None:
errors = await self.validate_input(user_input)
if not errors:
return self.async_create_entry(
data=user_input,
title=user_input[CONF_ADX_CLUSTER_INGEST_URI].replace(
"https://", ""
),
title=f"{user_input[CONF_ADX_CLUSTER_INGEST_URI].replace('https://', '')} / {user_input[CONF_ADX_DATABASE_NAME]} ({user_input[CONF_ADX_TABLE_NAME]})",
options=DEFAULT_OPTIONS,
)
# Keep previously entered values when we re-show the form after an error.
data_schema = self.add_suggested_values_to_schema(
STEP_USER_DATA_SCHEMA, user_input
)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
data_schema=data_schema,
errors=errors,
last_step=True,
)
@@ -20,6 +20,7 @@
"use_queued_ingestion": "Use queued ingestion"
},
"data_description": {
"authority_id": "In Azure portal this is also known as Directory (tenant) ID",
"cluster_ingest_uri": "Ingestion URI of the cluster",
"use_queued_ingestion": "Must be enabled when using ADX free cluster"
},
@@ -4,6 +4,7 @@ from __future__ import annotations
import json
import logging
from typing import Any
from azure.servicebus import ServiceBusMessage
from azure.servicebus.aio import ServiceBusClient, ServiceBusSender
@@ -92,7 +93,7 @@ class ServiceBusNotificationService(BaseNotificationService):
"""Initialize the service."""
self._client = client
async def async_send_message(self, message, **kwargs):
async def async_send_message(self, message: str, **kwargs: Any) -> None:
"""Send a message."""
dto = {ATTR_ASB_MESSAGE: message}
@@ -6,13 +6,15 @@ from datetime import timedelta
import logging
from typing import Any
from b2sdk.v2 import B2Api, Bucket, InMemoryAccountInfo, exception
from b2sdk.v2 import Bucket, exception
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.event import async_track_time_interval
# Import from b2_client to ensure timeout configuration is applied
from .b2_client import B2Api, InMemoryAccountInfo
from .const import (
BACKBLAZE_REALM,
CONF_APPLICATION_KEY,
@@ -72,7 +74,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
translation_domain=DOMAIN,
translation_key="invalid_bucket_name",
) from err
except exception.ConnectionReset as err:
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,
exception.ConnectionReset,
) as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="cannot_connect",
@@ -0,0 +1,39 @@
"""Backblaze B2 client with extended timeouts.
The b2sdk library uses class-level timeout attributes. To avoid modifying
global library state, we subclass the relevant classes to provide extended
timeouts suitable for backup operations involving large files.
"""
from b2sdk.v2 import B2Api as BaseB2Api, InMemoryAccountInfo
from b2sdk.v2.b2http import B2Http as BaseB2Http
from b2sdk.v2.session import B2Session as BaseB2Session
# Extended timeouts for Home Assistant backup operations
# Default CONNECTION_TIMEOUT is 46 seconds, which can be too short for slow connections
CONNECTION_TIMEOUT = 120 # 2 minutes
# Default TIMEOUT_FOR_UPLOAD is 128 seconds, which is too short for large backups
TIMEOUT_FOR_UPLOAD = 43200 # 12 hours
class B2Http(BaseB2Http): # type: ignore[misc]
"""B2Http with extended timeouts for backup operations."""
CONNECTION_TIMEOUT = CONNECTION_TIMEOUT
TIMEOUT_FOR_UPLOAD = TIMEOUT_FOR_UPLOAD
class B2Session(BaseB2Session): # type: ignore[misc]
"""B2Session using custom B2Http with extended timeouts."""
B2HTTP_CLASS = B2Http
class B2Api(BaseB2Api): # type: ignore[misc]
"""B2Api using custom session with extended timeouts."""
SESSION_CLASS = B2Session
__all__ = ["B2Api", "InMemoryAccountInfo"]
@@ -36,6 +36,10 @@ _LOGGER = logging.getLogger(__name__)
# Cache TTL for backup list (in seconds)
CACHE_TTL = 300
# Timeout for upload operations (in seconds)
# This prevents uploads from hanging indefinitely
UPLOAD_TIMEOUT = 43200 # 12 hours (matches B2 HTTP timeout)
def suggested_filenames(backup: AgentBackup) -> tuple[str, str]:
"""Return the suggested filenames for the backup and metadata files."""
@@ -329,13 +333,28 @@ class BackblazeBackupAgent(BackupAgent):
_LOGGER.debug("Uploading backup file %s with streaming", filename)
try:
content_type, _ = mimetypes.guess_type(filename)
file_version = await self._hass.async_add_executor_job(
self._upload_unbound_stream_sync,
reader,
filename,
content_type or "application/x-tar",
file_info,
file_version = await asyncio.wait_for(
self._hass.async_add_executor_job(
self._upload_unbound_stream_sync,
reader,
filename,
content_type or "application/x-tar",
file_info,
),
timeout=UPLOAD_TIMEOUT,
)
except TimeoutError:
_LOGGER.error(
"Upload of %s timed out after %s seconds", filename, UPLOAD_TIMEOUT
)
reader.abort()
raise BackupAgentError(
f"Upload timed out after {UPLOAD_TIMEOUT} seconds"
) from None
except asyncio.CancelledError:
_LOGGER.warning("Upload of %s was cancelled", filename)
reader.abort()
raise
finally:
reader.close()
@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from b2sdk.v2 import B2Api, InMemoryAccountInfo, exception
from b2sdk.v2 import exception
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
@@ -17,6 +17,8 @@ from homeassistant.helpers.selector import (
TextSelectorType,
)
# Import from b2_client to ensure timeout configuration is applied
from .b2_client import B2Api, InMemoryAccountInfo
from .const import (
BACKBLAZE_REALM,
CONF_APPLICATION_KEY,
@@ -172,8 +174,12 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
)
errors[CONF_BUCKET] = "invalid_bucket_name"
except exception.ConnectionReset:
_LOGGER.error("Failed to connect to Backblaze B2. Connection reset")
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,
exception.ConnectionReset,
) as err:
_LOGGER.error("Failed to connect to Backblaze B2: %s", err)
errors["base"] = "cannot_connect"
except exception.MissingAccountData:
# This generally indicates an issue with how InMemoryAccountInfo is used
@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["b2sdk"],
"quality_scale": "bronze",
"requirements": ["b2sdk==2.8.1"]
"requirements": ["b2sdk==2.10.1"]
}
@@ -34,7 +34,12 @@ class BeoData:
type BeoConfigEntry = ConfigEntry[BeoData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.EVENT,
Platform.MEDIA_PLAYER,
Platform.SENSOR,
]
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
@@ -0,0 +1,63 @@
"""Binary Sensor entities for the Bang & Olufsen integration."""
from __future__ import annotations
from mozart_api.models import BatteryState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
from .entity import BeoEntity
from .util import supports_battery
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Binary Sensor entities from config entry."""
if await supports_battery(config_entry.runtime_data.client):
async_add_entities(new_entities=[BeoBinarySensorBatteryCharging(config_entry)])
class BeoBinarySensorBatteryCharging(BinarySensorEntity, BeoEntity):
"""Battery charging Binary Sensor."""
_attr_device_class = BinarySensorDeviceClass.BATTERY_CHARGING
_attr_is_on = False
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Init the battery charging Binary Sensor."""
super().__init__(config_entry, config_entry.runtime_data.client)
self._attr_unique_id = f"{self._unique_id}_charging"
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
self._update_battery_charging,
)
)
async def _update_battery_charging(self, data: BatteryState) -> None:
"""Update battery charging."""
self._attr_is_on = bool(data.is_charging)
self.async_write_ha_state()
@@ -115,6 +115,7 @@ class WebsocketNotification(StrEnum):
"""Enum for WebSocket notification types."""
ACTIVE_LISTENING_MODE = "active_listening_mode"
BATTERY = "battery"
BEO_REMOTE_BUTTON = "beo_remote_button"
BUTTON = "button"
PLAYBACK_ERROR = "playback_error"
@@ -4,15 +4,17 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import BeoConfigEntry
from .const import DOMAIN
from .util import get_device_buttons
from .util import get_device_buttons, get_remote_keys, get_remotes
async def async_get_config_entry_diagnostics(
@@ -53,4 +55,58 @@ async def async_get_config_entry_diagnostics(
state_dict.pop("context")
data[f"{device_button}_event"] = state_dict
# Get remotes
for remote in await get_remotes(config_entry.runtime_data.client):
# Get Battery Sensor states
if entity_id := entity_registry.async_get_entity_id(
SENSOR_DOMAIN,
DOMAIN,
f"{remote.serial_number}_{config_entry.unique_id}_remote_battery_level",
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data[f"remote_{remote.serial_number}_battery_level"] = state_dict
# Get key Event entity states (if enabled)
for key_type in get_remote_keys():
if entity_id := entity_registry.async_get_entity_id(
EVENT_DOMAIN,
DOMAIN,
f"{remote.serial_number}_{config_entry.unique_id}_{key_type}",
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data[f"remote_{remote.serial_number}_{key_type}_event"] = state_dict
# Add remote Mozart model
data[f"remote_{remote.serial_number}"] = dict(remote)
# Get Mozart battery entity
if entity_id := entity_registry.async_get_entity_id(
SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_battery_level"
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["battery_level"] = state_dict
# Get Mozart battery charging entity
if entity_id := entity_registry.async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, f"{config_entry.unique_id}_charging"
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["charging"] = state_dict
return data
+5 -28
View File
@@ -16,11 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS,
BEO_REMOTE_KEYS,
BEO_REMOTE_SUBMENU_CONTROL,
BEO_REMOTE_SUBMENU_LIGHT,
CONNECTION_STATUS,
DEVICE_BUTTON_EVENTS,
DOMAIN,
@@ -29,7 +25,7 @@ from .const import (
WebsocketNotification,
)
from .entity import BeoEntity
from .util import get_device_buttons, get_remotes
from .util import get_device_buttons, get_remote_keys, get_remotes
PARALLEL_UPDATES = 0
@@ -40,38 +36,19 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Event entities from config entry."""
entities: list[BeoEvent] = []
async_add_entities(
entities: list[BeoEvent] = [
BeoButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
)
]
# Check for connected Beoremote One
remotes = await get_remotes(config_entry.runtime_data.client)
for remote in remotes:
# Add Light keys
entities.extend(
[
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
)
for key_type in BEO_REMOTE_KEYS
]
)
# Add Control keys
entities.extend(
[
BeoRemoteKeyEvent(
config_entry,
remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
)
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
BeoRemoteKeyEvent(config_entry, remote, key_type)
for key_type in get_remote_keys()
]
)
@@ -0,0 +1,139 @@
"""Sensor entities for the Bang & Olufsen integration."""
from __future__ import annotations
import contextlib
from datetime import timedelta
from aiohttp import ClientConnectorError
from mozart_api.exceptions import ApiException
from mozart_api.models import BatteryState, PairedRemote
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry
from .const import CONNECTION_STATUS, DOMAIN, WebsocketNotification
from .entity import BeoEntity
from .util import get_remotes, supports_battery
SCAN_INTERVAL = timedelta(minutes=15)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BeoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Sensor entities from config entry."""
entities: list[BeoSensor] = []
# Check for Mozart device with battery
if await supports_battery(config_entry.runtime_data.client):
entities.append(BeoSensorBatteryLevel(config_entry))
# Add any Beoremote One remotes
entities.extend(
[
BeoSensorRemoteBatteryLevel(config_entry, remote)
for remote in (await get_remotes(config_entry.runtime_data.client))
]
)
async_add_entities(entities, update_before_add=True)
class BeoSensor(SensorEntity, BeoEntity):
"""Base Bang & Olufsen Sensor."""
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Initialize Sensor."""
super().__init__(config_entry, config_entry.runtime_data.client)
class BeoSensorBatteryLevel(BeoSensor):
"""Battery level Sensor for Mozart devices."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, config_entry: BeoConfigEntry) -> None:
"""Init the battery level Sensor."""
super().__init__(config_entry)
self._attr_unique_id = f"{self._unique_id}_battery_level"
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
self._update_battery,
)
)
async def _update_battery(self, data: BatteryState) -> None:
"""Update sensor value."""
self._attr_native_value = data.battery_level
self.async_write_ha_state()
class BeoSensorRemoteBatteryLevel(BeoSensor):
"""Battery level Sensor for the Beoremote One."""
_attr_device_class = SensorDeviceClass.BATTERY
_attr_native_unit_of_measurement = PERCENTAGE
_attr_should_poll = True
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(self, config_entry: BeoConfigEntry, remote: PairedRemote) -> None:
"""Init the battery level Sensor."""
super().__init__(config_entry)
# Serial number is not None, as the remote object is provided by get_remotes
assert remote.serial_number
self._attr_unique_id = (
f"{remote.serial_number}_{self._unique_id}_remote_battery_level"
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}
)
self._attr_native_value = remote.battery_level
self._remote = remote
async def async_added_to_hass(self) -> None:
"""Turn on the dispatchers."""
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{DOMAIN}_{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
async def async_update(self) -> None:
"""Poll battery status."""
with contextlib.suppress(ApiException, ClientConnectorError, TimeoutError):
for remote in await get_remotes(self._client):
if remote.serial_number == self._remote.serial_number:
self._attr_native_value = remote.battery_level
break
+28 -1
View File
@@ -11,7 +11,16 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel
from .const import (
BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEYS,
BEO_REMOTE_SUBMENU_CONTROL,
BEO_REMOTE_SUBMENU_LIGHT,
DEVICE_BUTTONS,
DOMAIN,
BeoButtons,
BeoModel,
)
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -64,3 +73,21 @@ def get_device_buttons(model: BeoModel) -> list[str]:
buttons.remove(BeoButtons.BLUETOOTH)
return buttons
def get_remote_keys() -> list[str]:
"""Get remote keys for the Beoremote One. Formatted for Home Assistant use."""
return [
*[f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}" for key_type in BEO_REMOTE_KEYS],
*[
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}"
for key_type in (*BEO_REMOTE_KEYS, *BEO_REMOTE_CONTROL_KEYS)
],
]
async def supports_battery(client: MozartClient) -> bool:
"""Get if a Mozart device has a battery."""
battery_state = await client.get_battery_state()
return battery_state.state != "BatteryNotPresent"
@@ -6,6 +6,7 @@ import logging
from typing import TYPE_CHECKING
from mozart_api.models import (
BatteryState,
BeoRemoteButton,
ButtonEvent,
ListeningModeProps,
@@ -60,6 +61,7 @@ class BeoWebsocket(BeoBase):
self._client.get_active_listening_mode_notifications(
self.on_active_listening_mode
)
self._client.get_battery_notifications(self.on_battery_notification)
self._client.get_beo_remote_button_notifications(
self.on_beo_remote_button_notification
)
@@ -115,6 +117,14 @@ class BeoWebsocket(BeoBase):
notification,
)
def on_battery_notification(self, notification: BatteryState) -> None:
"""Send battery dispatch."""
async_dispatcher_send(
self.hass,
f"{DOMAIN}_{self._unique_id}_{WebsocketNotification.BATTERY}",
notification,
)
def on_beo_remote_button_notification(self, notification: BeoRemoteButton) -> None:
"""Send beo_remote_button dispatch."""
if TYPE_CHECKING:
@@ -47,7 +47,7 @@ def make_binary_sensor_trigger(
"""Trigger for entity state changes."""
_device_class = device_class
_to_state = to_state
_to_states = {to_state}
return CustomTrigger
@@ -2,7 +2,6 @@
"domain": "blackbird",
"name": "Monoprice Blackbird Matrix Switch",
"codeowners": [],
"disabled": "This integration is disabled because it references pyserial-asyncio, which does blocking I/O in the asyncio loop and is not maintained.",
"documentation": "https://www.home-assistant.io/integrations/blackbird",
"iot_class": "local_polling",
"loggers": ["pyblackbird"],
+43 -2
View File
@@ -2,16 +2,25 @@
from pyblu import Player
from pyblu.errors import PlayerUnreachableError
import voluptuous as vol
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, service
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
from .const import (
ATTR_MASTER,
DOMAIN,
SERVICE_CLEAR_TIMER,
SERVICE_JOIN,
SERVICE_SET_TIMER,
SERVICE_UNJOIN,
)
from .coordinator import (
BluesoundConfigEntry,
BluesoundCoordinator,
@@ -28,6 +37,38 @@ PLATFORMS = [
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Bluesound."""
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_SET_TIMER,
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="async_increase_timer",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_CLEAR_TIMER,
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="async_clear_timer",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_JOIN,
entity_domain=MEDIA_PLAYER_DOMAIN,
schema={vol.Required(ATTR_MASTER): cv.entity_id},
func="async_bluesound_join",
)
service.async_register_platform_entity_service(
hass,
DOMAIN,
SERVICE_UNJOIN,
entity_domain=MEDIA_PLAYER_DOMAIN,
schema=None,
func="async_bluesound_unjoin",
)
return True
@@ -4,3 +4,8 @@ DOMAIN = "bluesound"
INTEGRATION_TITLE = "Bluesound"
ATTR_BLUESOUND_GROUP = "bluesound_group"
ATTR_MASTER = "master"
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
SERVICE_JOIN = "join"
SERVICE_SET_TIMER = "set_sleep_timer"
SERVICE_UNJOIN = "unjoin"
@@ -8,7 +8,6 @@ import logging
from typing import TYPE_CHECKING, Any
from pyblu import Input, Player, Preset, Status, SyncStatus
import voluptuous as vol
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -22,11 +21,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers import entity_registry as er, issue_registry as ir
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -40,9 +35,22 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util, slugify
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
from .const import (
ATTR_BLUESOUND_GROUP,
ATTR_MASTER,
DOMAIN,
SERVICE_CLEAR_TIMER,
SERVICE_JOIN,
SERVICE_SET_TIMER,
SERVICE_UNJOIN,
)
from .coordinator import BluesoundCoordinator
from .utils import dispatcher_join_signal, dispatcher_unjoin_signal, format_unique_id
from .utils import (
dispatcher_join_signal,
dispatcher_unjoin_signal,
format_unique_id,
id_to_paired_player,
)
if TYPE_CHECKING:
from . import BluesoundConfigEntry
@@ -54,11 +62,6 @@ SCAN_INTERVAL = timedelta(minutes=15)
DATA_BLUESOUND = DOMAIN
DEFAULT_PORT = 11000
SERVICE_CLEAR_TIMER = "clear_sleep_timer"
SERVICE_JOIN = "join"
SERVICE_SET_TIMER = "set_sleep_timer"
SERVICE_UNJOIN = "unjoin"
POLL_TIMEOUT = 120
@@ -75,18 +78,6 @@ async def async_setup_entry(
config_entry.runtime_data.player,
)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(
SERVICE_SET_TIMER, None, "async_increase_timer"
)
platform.async_register_entity_service(
SERVICE_CLEAR_TIMER, None, "async_clear_timer"
)
platform.async_register_entity_service(
SERVICE_JOIN, {vol.Required(ATTR_MASTER): cv.entity_id}, "async_join"
)
platform.async_register_entity_service(SERVICE_UNJOIN, None, "async_unjoin")
async_add_entities([bluesound_player], update_before_add=True)
@@ -120,6 +111,7 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
self._presets: list[Preset] = coordinator.data.presets
self._group_name: str | None = None
self._group_list: list[str] = []
self._group_members: list[str] | None = None
self._bluesound_device_name = sync_status.name
self._player = player
self._last_status_update = dt_util.utcnow()
@@ -180,6 +172,7 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
self._last_status_update = dt_util.utcnow()
self._group_list = self.rebuild_bluesound_group()
self._group_members = self.rebuild_group_members()
self.async_write_ha_state()
@@ -365,11 +358,13 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.GROUPING
)
supported = (
MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.GROUPING
)
if not self._status.indexing:
@@ -421,8 +416,57 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
return shuffle
async def async_join(self, master: str) -> None:
@property
def group_members(self) -> list[str] | None:
"""Get list of group members. Leader is always first."""
return self._group_members
async def async_join_players(self, group_members: list[str]) -> None:
"""Join `group_members` as a player group with the current player."""
if self.entity_id in group_members:
raise ServiceValidationError("Cannot join player to itself")
entity_ids_with_sync_status = self._entity_ids_with_sync_status()
paired_players = []
for group_member in group_members:
sync_status = entity_ids_with_sync_status.get(group_member)
if sync_status is None:
continue
paired_player = id_to_paired_player(sync_status.id)
if paired_player:
paired_players.append(paired_player)
if paired_players:
await self._player.add_followers(paired_players)
async def async_unjoin_player(self) -> None:
"""Remove this player from any group."""
if self._sync_status.leader is not None:
leader_id = f"{self._sync_status.leader.ip}:{self._sync_status.leader.port}"
async_dispatcher_send(
self.hass, dispatcher_unjoin_signal(leader_id), self.host, self.port
)
if self._sync_status.followers is not None:
await self._player.remove_follower(self.host, self.port)
async def async_bluesound_join(self, master: str) -> None:
"""Join the player to a group."""
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_JOIN}",
is_fixable=False,
breaks_in_ha_version="2026.7.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_join",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
if master == self.entity_id:
raise ServiceValidationError("Cannot join player to itself")
@@ -431,18 +475,24 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
self.hass, dispatcher_join_signal(master), self.host, self.port
)
async def async_unjoin(self) -> None:
async def async_bluesound_unjoin(self) -> None:
"""Unjoin the player from a group."""
if self._sync_status.leader is None:
return
leader_id = f"{self._sync_status.leader.ip}:{self._sync_status.leader.port}"
_LOGGER.debug("Trying to unjoin player: %s", self.id)
async_dispatcher_send(
self.hass, dispatcher_unjoin_signal(leader_id), self.host, self.port
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_UNJOIN}",
is_fixable=False,
breaks_in_ha_version="2026.7.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_unjoin",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
await self.async_unjoin_player()
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""List members in group."""
@@ -488,6 +538,63 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
follower_names.insert(0, leader_sync_status.name)
return follower_names
def rebuild_group_members(self) -> list[str] | None:
"""Get list of group members. Leader is always first."""
if self.sync_status.leader is None and self.sync_status.followers is None:
return None
entity_ids_with_sync_status = self._entity_ids_with_sync_status()
leader_entity_id = None
followers = None
if self.sync_status.followers is not None:
leader_entity_id = self.entity_id
followers = self.sync_status.followers
elif self.sync_status.leader is not None:
leader_id = f"{self.sync_status.leader.ip}:{self.sync_status.leader.port}"
for entity_id, sync_status in entity_ids_with_sync_status.items():
if sync_status.id == leader_id:
leader_entity_id = entity_id
followers = sync_status.followers
break
if leader_entity_id is None or followers is None:
return None
grouped_entity_ids = [leader_entity_id]
for follower in followers:
follower_id = f"{follower.ip}:{follower.port}"
entity_ids = [
entity_id
for entity_id, sync_status in entity_ids_with_sync_status.items()
if sync_status.id == follower_id
]
match entity_ids:
case [entity_id]:
grouped_entity_ids.append(entity_id)
return grouped_entity_ids
def _entity_ids_with_sync_status(self) -> dict[str, SyncStatus]:
result = {}
entity_registry = er.async_get(self.hass)
config_entries: list[BluesoundConfigEntry] = (
self.hass.config_entries.async_entries(DOMAIN)
)
for config_entry in config_entries:
entity_entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
for entity_entry in entity_entries:
if entity_entry.domain == "media_player":
result[entity_entry.entity_id] = (
config_entry.runtime_data.coordinator.data.sync_status
)
return result
async def async_add_follower(self, host: str, port: int) -> None:
"""Add follower to leader."""
await self._player.add_follower(host, port)
@@ -41,9 +41,17 @@
"description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts.",
"title": "Detected use of deprecated action bluesound.clear_sleep_timer"
},
"deprecated_service_join": {
"description": "Use the `media_player.join` action instead.\n\nPlease replace this action and adjust your automations and scripts.",
"title": "Detected use of deprecated action bluesound.join"
},
"deprecated_service_set_sleep_timer": {
"description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts.",
"title": "Detected use of deprecated action bluesound.set_sleep_timer"
},
"deprecated_service_unjoin": {
"description": "Use the `media_player.unjoin` action instead.\n\nPlease replace this action and adjust your automations and scripts.",
"title": "Detected use of deprecated action bluesound.unjoin"
}
},
"services": {
@@ -1,5 +1,7 @@
"""Utility functions for the Bluesound component."""
from pyblu import PairedPlayer
from homeassistant.helpers.device_registry import format_mac
@@ -19,3 +21,12 @@ def dispatcher_unjoin_signal(leader_id: str) -> str:
Id is ip_address:port. This can be obtained from sync_status.id.
"""
return f"bluesound_unjoin_{leader_id}"
def id_to_paired_player(id: str) -> PairedPlayer | None:
"""Try to convert id in format 'ip:port' to PairedPlayer. Returns None if unable to do so."""
match id.rsplit(":", 1):
case [str() as ip, str() as port] if port.isdigit():
return PairedPlayer(ip, int(port))
case _:
return None
@@ -11,6 +11,7 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import CONF_USE_SSL
from .coordinator import BraviaTVConfigEntry, BraviaTVCoordinator
PLATFORMS: Final[list[Platform]] = [
@@ -26,11 +27,12 @@ async def async_setup_entry(
"""Set up a config entry."""
host = config_entry.data[CONF_HOST]
mac = config_entry.data[CONF_MAC]
ssl = config_entry.data.get(CONF_USE_SSL, False)
session = async_create_clientsession(
hass, cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
)
client = BraviaClient(host, mac, session=session)
client = BraviaClient(host, mac, session=session, ssl=ssl)
coordinator = BraviaTVCoordinator(
hass=hass,
config_entry=config_entry,
@@ -28,6 +28,7 @@ from .const import (
ATTR_MODEL,
CONF_NICKNAME,
CONF_USE_PSK,
CONF_USE_SSL,
DOMAIN,
NICKNAME_PREFIX,
)
@@ -46,11 +47,12 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
def create_client(self) -> None:
"""Create Bravia TV client from config."""
host = self.device_config[CONF_HOST]
ssl = self.device_config[CONF_USE_SSL]
session = async_create_clientsession(
self.hass,
cookie_jar=CookieJar(unsafe=True, quote_cookie=False),
)
self.client = BraviaClient(host=host, session=session)
self.client = BraviaClient(host=host, session=session, ssl=ssl)
async def gen_instance_ids(self) -> tuple[str, str]:
"""Generate client_id and nickname."""
@@ -123,10 +125,10 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle authorize step."""
self.create_client()
if user_input is not None:
self.device_config[CONF_USE_PSK] = user_input[CONF_USE_PSK]
self.device_config[CONF_USE_SSL] = user_input[CONF_USE_SSL]
self.create_client()
if user_input[CONF_USE_PSK]:
return await self.async_step_psk()
return await self.async_step_pin()
@@ -136,6 +138,7 @@ class BraviaTVConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{
vol.Required(CONF_USE_PSK, default=False): bool,
vol.Required(CONF_USE_SSL, default=False): bool,
}
),
)
@@ -12,6 +12,7 @@ ATTR_MODEL: Final = "model"
CONF_NICKNAME: Final = "nickname"
CONF_USE_PSK: Final = "use_psk"
CONF_USE_SSL: Final = "use_ssl"
DOMAIN: Final = "braviatv"
LEGACY_CLIENT_ID: Final = "HomeAssistant"
@@ -22,7 +22,7 @@ from homeassistant.components.media_player import MediaType
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_CLIENT_ID, CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -56,8 +56,31 @@ def catch_braviatv_errors[_BraviaTVCoordinatorT: BraviaTVCoordinator, **_P](
"""Catch Bravia errors and log message."""
try:
await func(self, *args, **kwargs)
except BraviaNotFound as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error_not_found",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error_offline",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
except BraviaError as err:
_LOGGER.error("Command error: %s", err)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="command_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
await self.async_request_refresh()
return wrapper
@@ -165,17 +188,35 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
if self.skipped_updates < 10:
self.connected = False
self.skipped_updates += 1
_LOGGER.debug("Update skipped, Bravia API service is reloading")
_LOGGER.debug(
"Update for %s skipped: the Bravia API service is reloading",
self.config_entry.title,
)
return
raise UpdateFailed("Error communicating with device") from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error_not_found",
translation_placeholders={
"device": self.config_entry.title,
},
) from err
except (BraviaConnectionError, BraviaConnectionTimeout, BraviaTurnedOff):
self.is_on = False
self.connected = False
_LOGGER.debug("Update skipped, Bravia TV is off")
_LOGGER.debug(
"Update for %s skipped: the TV is turned off", self.config_entry.title
)
except BraviaError as err:
self.is_on = False
self.connected = False
raise UpdateFailed("Error communicating with device") from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_error",
translation_placeholders={
"device": self.config_entry.title,
"error": repr(err),
},
) from err
async def async_update_volume(self) -> None:
"""Update volume information."""
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pybravia"],
"requirements": ["pybravia==0.3.4"],
"requirements": ["pybravia==0.4.1"],
"ssdp": [
{
"manufacturer": "Sony Corporation",
+20 -2
View File
@@ -15,9 +15,10 @@
"step": {
"authorize": {
"data": {
"use_psk": "Use PSK authentication"
"use_psk": "Use PSK authentication",
"use_ssl": "Use SSL connection"
},
"description": "Make sure that «Control remotely» is enabled on your TV, go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended as more stable.",
"description": "Make sure that «Control remotely» is enabled on your TV. Go to: \nSettings -> Network -> Remote device settings -> Control remotely. \n\nThere are two authorization methods: PIN code or PSK (Pre-Shared Key). \nAuthorization via PSK is recommended, as it is more stable. \n\nUse an SSL connection only if your TV supports this connection type.",
"title": "Authorize Sony Bravia TV"
},
"confirm": {
@@ -54,5 +55,22 @@
"name": "Terminate apps"
}
}
},
"exceptions": {
"command_error": {
"message": "Error sending command to {device}: {error}"
},
"command_error_not_found": {
"message": "Error sending command to {device}: the Bravia API service is reloading"
},
"command_error_offline": {
"message": "Error sending command to {device}: the TV is turned off"
},
"update_error": {
"message": "Error updating data for {device}: {error}"
},
"update_error_not_found": {
"message": "Error updating data for {device}: the Bravia API service is stuck"
}
}
}
+23 -7
View File
@@ -1,5 +1,6 @@
"""The BSB-Lan integration."""
import asyncio
import dataclasses
from bsblan import (
@@ -27,13 +28,18 @@ from homeassistant.exceptions import (
ConfigEntryError,
ConfigEntryNotReady,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PASSKEY, DOMAIN
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
PLATFORMS = [Platform.CLIMATE, Platform.SENSOR, Platform.WATER_HEATER]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type BSBLanConfigEntry = ConfigEntry[BSBLanData]
@@ -49,6 +55,12 @@ class BSBLanData:
static: StaticState
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the BSB-Lan integration."""
async_setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bool:
"""Set up BSB-Lan from a config entry."""
@@ -66,12 +78,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
bsblan = BSBLAN(config, session)
try:
# Initialize the client first - this sets up internal caches and validates the connection
# Initialize the client first - this sets up internal caches and validates
# the connection by fetching firmware version
await bsblan.initialize()
# Fetch all required device metadata
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
# Fetch device metadata in parallel for faster startup
device, info, static = await asyncio.gather(
bsblan.device(),
bsblan.info(),
bsblan.static_values(),
)
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
@@ -99,10 +115,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)
# Perform first refresh of both coordinators
# Perform first refresh of fast coordinator (required for entities)
await fast_coordinator.async_config_entry_first_refresh()
# Try to refresh slow coordinator, but don't fail if DHW is not available
# Refresh slow coordinator - don't fail if DHW is not available
# This allows the integration to work even if the device doesn't support DHW
await slow_coordinator.async_refresh()
+9 -4
View File
@@ -111,11 +111,17 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
return None
return self.coordinator.data.state.target_temperature.value
@property
def _hvac_mode_value(self) -> int | str | None:
"""Return the raw hvac_mode value from the coordinator."""
if (hvac_mode := self.coordinator.data.state.hvac_mode) is None:
return None
return hvac_mode.value
@property
def hvac_mode(self) -> HVACMode | None:
"""Return hvac operation ie. heat, cool mode."""
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
if hvac_mode_value is None:
if (hvac_mode_value := self._hvac_mode_value) is None:
return None
# BSB-Lan returns integer values: 0=off, 1=auto, 2=eco, 3=heat
if isinstance(hvac_mode_value, int):
@@ -125,9 +131,8 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
hvac_mode_value = self.coordinator.data.state.hvac_mode.value
# BSB-Lan mode 2 is eco/reduced mode
if hvac_mode_value == 2:
if self._hvac_mode_value == 2:
return PRESET_ECO
return PRESET_NONE
+19 -20
View File
@@ -2,7 +2,6 @@
from dataclasses import dataclass
from datetime import timedelta
from random import randint
from bsblan import (
BSBLAN,
@@ -23,6 +22,17 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, LOGGER, SCAN_INTERVAL_FAST, SCAN_INTERVAL_SLOW
# Filter lists for optimized API calls - only fetch parameters we actually use
# This significantly reduces response time (~0.2s per parameter saved)
STATE_INCLUDE = ["current_temperature", "target_temperature", "hvac_mode"]
SENSOR_INCLUDE = ["current_temperature", "outside_temperature"]
DHW_STATE_INCLUDE = [
"operating_mode",
"nominal_setpoint",
"dhw_actual_value_top_temperature",
]
DHW_CONFIG_INCLUDE = ["reduced_setpoint", "nominal_setpoint_max"]
@dataclass
class BSBLanFastData:
@@ -80,26 +90,18 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
config_entry,
client,
name=f"{DOMAIN}_fast_{config_entry.data[CONF_HOST]}",
update_interval=self._get_update_interval(),
update_interval=SCAN_INTERVAL_FAST,
)
def _get_update_interval(self) -> timedelta:
"""Get the update interval with a random offset.
Add a random number of seconds to avoid timeouts when
the BSB-Lan device is already/still busy retrieving data,
e.g. for MQTT or internal logging.
"""
return SCAN_INTERVAL_FAST + timedelta(seconds=randint(1, 8))
async def _async_update_data(self) -> BSBLanFastData:
"""Fetch fast-changing data from the BSB-Lan device."""
try:
# Client is already initialized in async_setup_entry
# Fetch fast-changing data (state, sensor, DHW state)
state = await self.client.state()
sensor = await self.client.sensor()
dhw = await self.client.hot_water_state()
# Use include filtering to only fetch parameters we actually use
# This reduces response time significantly (~0.2s per parameter)
state = await self.client.state(include=STATE_INCLUDE)
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
@@ -111,9 +113,6 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
f"Error while establishing connection with BSB-Lan device at {host}"
) from err
# Update the interval with random jitter for next update
self.update_interval = self._get_update_interval()
return BSBLanFastData(
state=state,
sensor=sensor,
@@ -143,8 +142,8 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
"""Fetch slow-changing data from the BSB-Lan device."""
try:
# Client is already initialized in async_setup_entry
# Fetch slow-changing configuration data
dhw_config = await self.client.hot_water_config()
# Use include filtering to only fetch parameters we actually use
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
dhw_schedule = await self.client.hot_water_schedule()
except AttributeError:
+5 -1
View File
@@ -29,7 +29,11 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
name=data.device.name,
manufacturer="BSBLAN Inc.",
model=data.info.device_identification.value,
model=(
data.info.device_identification.value
if data.info.device_identification
else None
),
sw_version=data.device.version,
configuration_url=f"http://{host}",
)
@@ -0,0 +1,10 @@
{
"services": {
"set_hot_water_schedule": {
"service": "mdi:calendar-clock"
},
"sync_time": {
"service": "mdi:timer-sync-outline"
}
}
}
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==3.1.4"],
"requirements": ["python-bsblan==4.1.0"],
"zeroconf": [
{
"name": "bsb-lan*",
+291
View File
@@ -0,0 +1,291 @@
"""Support for BSB-Lan services."""
from __future__ import annotations
from datetime import time
import logging
from typing import TYPE_CHECKING
from bsblan import BSBLANError, DaySchedule, DHWSchedule, TimeSlot
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.util import dt as dt_util
from .const import DOMAIN
if TYPE_CHECKING:
from . import BSBLanConfigEntry
LOGGER = logging.getLogger(__name__)
ATTR_DEVICE_ID = "device_id"
ATTR_MONDAY_SLOTS = "monday_slots"
ATTR_TUESDAY_SLOTS = "tuesday_slots"
ATTR_WEDNESDAY_SLOTS = "wednesday_slots"
ATTR_THURSDAY_SLOTS = "thursday_slots"
ATTR_FRIDAY_SLOTS = "friday_slots"
ATTR_SATURDAY_SLOTS = "saturday_slots"
ATTR_SUNDAY_SLOTS = "sunday_slots"
# Service names
SERVICE_SET_HOT_WATER_SCHEDULE = "set_hot_water_schedule"
SERVICE_SYNC_TIME = "sync_time"
# Schema for a single time slot
_SLOT_SCHEMA = vol.Schema(
{
vol.Required("start_time"): cv.time,
vol.Required("end_time"): cv.time,
}
)
SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): cv.string,
vol.Optional(ATTR_MONDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_TUESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_WEDNESDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_THURSDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_FRIDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_SATURDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
vol.Optional(ATTR_SUNDAY_SLOTS): vol.All(cv.ensure_list, [_SLOT_SCHEMA]),
}
)
def _convert_time_slots_to_day_schedule(
slots: list[dict[str, time]] | None,
) -> DaySchedule | None:
"""Convert list of time slot dicts to a DaySchedule object.
Example: [{"start_time": time(6, 0), "end_time": time(8, 0)},
{"start_time": time(17, 0), "end_time": time(21, 0)}]
becomes: DaySchedule with two TimeSlot objects
None returns None (don't modify this day).
Empty list returns DaySchedule with empty slots (clear this day).
"""
if slots is None:
return None
if not slots:
return DaySchedule(slots=[])
time_slots = []
for slot in slots:
start_time = slot["start_time"]
end_time = slot["end_time"]
# Validate that end time is after start time
if end_time <= start_time:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="end_time_before_start_time",
translation_placeholders={
"start_time": start_time.strftime("%H:%M"),
"end_time": end_time.strftime("%H:%M"),
},
)
time_slots.append(TimeSlot(start=start_time, end=end_time))
LOGGER.debug(
"Created time slot: %s-%s",
start_time.strftime("%H:%M"),
end_time.strftime("%H:%M"),
)
LOGGER.debug("Created DaySchedule with %d slots", len(time_slots))
return DaySchedule(slots=time_slots)
async def set_hot_water_schedule(service_call: ServiceCall) -> None:
"""Set hot water heating schedule."""
device_id = service_call.data[ATTR_DEVICE_ID]
# Get the device and config entry
device_registry = dr.async_get(service_call.hass)
device_entry = device_registry.async_get(device_id)
if device_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_id",
translation_placeholders={"device_id": device_id},
)
# Find the config entry for this device
matching_entries: list[BSBLanConfigEntry] = [
entry
for entry in service_call.hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
]
if not matching_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_config_entry_for_device",
translation_placeholders={"device_id": device_entry.name or device_id},
)
entry = matching_entries[0]
# Verify the config entry is loaded
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_loaded",
translation_placeholders={"device_name": device_entry.name or device_id},
)
client = entry.runtime_data.client
# Convert time slots to DaySchedule objects
monday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_MONDAY_SLOTS)
)
tuesday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_TUESDAY_SLOTS)
)
wednesday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_WEDNESDAY_SLOTS)
)
thursday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_THURSDAY_SLOTS)
)
friday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_FRIDAY_SLOTS)
)
saturday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_SATURDAY_SLOTS)
)
sunday = _convert_time_slots_to_day_schedule(
service_call.data.get(ATTR_SUNDAY_SLOTS)
)
# Create the DHWSchedule object
dhw_schedule = DHWSchedule(
monday=monday,
tuesday=tuesday,
wednesday=wednesday,
thursday=thursday,
friday=friday,
saturday=saturday,
sunday=sunday,
)
LOGGER.debug(
"Setting hot water schedule - Monday: %s, Tuesday: %s, Wednesday: %s, "
"Thursday: %s, Friday: %s, Saturday: %s, Sunday: %s",
monday,
tuesday,
wednesday,
thursday,
friday,
saturday,
sunday,
)
try:
# Call the BSB-Lan API to set the schedule
await client.set_hot_water_schedule(dhw_schedule)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="set_schedule_failed",
translation_placeholders={"error": str(err)},
) from err
# Refresh the slow coordinator to get the updated schedule
await entry.runtime_data.slow_coordinator.async_request_refresh()
async def async_sync_time(service_call: ServiceCall) -> None:
"""Synchronize BSB-LAN device time with Home Assistant."""
device_id: str = service_call.data[ATTR_DEVICE_ID]
# Get the device and config entry
device_registry = dr.async_get(service_call.hass)
device_entry = device_registry.async_get(device_id)
if device_entry is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_device_id",
translation_placeholders={"device_id": device_id},
)
# Find the config entry for this device
matching_entries: list[BSBLanConfigEntry] = [
entry
for entry in service_call.hass.config_entries.async_entries(DOMAIN)
if entry.entry_id in device_entry.config_entries
]
if not matching_entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_config_entry_for_device",
translation_placeholders={"device_id": device_entry.name or device_id},
)
entry = matching_entries[0]
# Verify the config entry is loaded
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_loaded",
translation_placeholders={"device_name": device_entry.name or device_id},
)
client = entry.runtime_data.client
try:
# Get current device time
device_time = await client.time()
current_time = dt_util.now()
current_time_str = current_time.strftime("%d.%m.%Y %H:%M:%S")
# Only sync if device time differs from HA time
if device_time.time.value != current_time_str:
await client.set_time(current_time_str)
except BSBLANError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="sync_time_failed",
translation_placeholders={
"device_name": device_entry.name or device_id,
"error": str(err),
},
) from err
SYNC_TIME_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DEVICE_ID): cv.string,
}
)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the BSB-Lan services."""
hass.services.async_register(
DOMAIN,
SERVICE_SET_HOT_WATER_SCHEDULE,
set_hot_water_schedule,
schema=SERVICE_SET_HOT_WATER_SCHEDULE_SCHEMA,
)
hass.services.async_register(
DOMAIN,
SERVICE_SYNC_TIME,
async_sync_time,
schema=SYNC_TIME_SCHEMA,
)
@@ -0,0 +1,122 @@
sync_time:
fields:
device_id:
required: true
example: "abc123device456"
selector:
device:
integration: bsblan
set_hot_water_schedule:
fields:
device_id:
required: true
example: "abc123device456"
selector:
device:
integration: bsblan
monday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
tuesday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
wednesday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
thursday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
friday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
saturday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
sunday_slots:
selector:
object:
multiple: true
label_field: start_time
description_field: end_time
fields:
start_time:
required: true
selector:
time:
end_time:
required: true
selector:
time:
@@ -70,6 +70,18 @@
}
},
"exceptions": {
"config_entry_not_loaded": {
"message": "The device `{device_name}` is not currently loaded or available"
},
"end_time_before_start_time": {
"message": "End time ({end_time}) must be after start time ({start_time})"
},
"invalid_device_id": {
"message": "Invalid device ID: {device_id}"
},
"no_config_entry_for_device": {
"message": "No configuration entry found for device: {device_id}"
},
"set_data_error": {
"message": "An error occurred while sending the data to the BSB-Lan device"
},
@@ -79,6 +91,9 @@
"set_preset_mode_error": {
"message": "Can't set preset mode to {preset_mode} when HVAC mode is not set to auto"
},
"set_schedule_failed": {
"message": "Failed to set hot water schedule: {error}"
},
"set_temperature_error": {
"message": "An error occurred while setting the temperature"
},
@@ -90,6 +105,59 @@
},
"setup_general_error": {
"message": "An unknown error occurred while retrieving static device data"
},
"sync_time_failed": {
"message": "Failed to sync time for {device_name}: {error}"
}
},
"services": {
"set_hot_water_schedule": {
"description": "Set the hot water heating schedule for a BSB-LAN device.",
"fields": {
"device_id": {
"description": "The BSB-LAN device to configure.",
"name": "Device"
},
"friday_slots": {
"description": "Time periods for Friday. Add multiple slots for different heating periods throughout the day.",
"name": "Friday time slots"
},
"monday_slots": {
"description": "Time periods for Monday. Add multiple slots for different heating periods throughout the day.",
"name": "Monday time slots"
},
"saturday_slots": {
"description": "Time periods for Saturday. Add multiple slots for different heating periods throughout the day.",
"name": "Saturday time slots"
},
"sunday_slots": {
"description": "Time periods for Sunday. Add multiple slots for different heating periods throughout the day.",
"name": "Sunday time slots"
},
"thursday_slots": {
"description": "Time periods for Thursday. Add multiple slots for different heating periods throughout the day.",
"name": "Thursday time slots"
},
"tuesday_slots": {
"description": "Time periods for Tuesday. Add multiple slots for different heating periods throughout the day.",
"name": "Tuesday time slots"
},
"wednesday_slots": {
"description": "Time periods for Wednesday. Add multiple slots for different heating periods throughout the day.",
"name": "Wednesday time slots"
}
},
"name": "Set hot water schedule"
},
"sync_time": {
"description": "Synchronize Home Assistant time to the BSB-Lan device. Only updates if device time differs from Home Assistant time.",
"fields": {
"device_id": {
"description": "The BSB-LAN device to sync time for.",
"name": "Device"
}
},
"name": "Sync time"
}
}
}
@@ -20,5 +20,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bthome",
"iot_class": "local_push",
"requirements": ["bthome-ble==3.15.0"]
"requirements": ["bthome-ble==3.16.0"]
}
@@ -15,5 +15,13 @@
"get_events": {
"service": "mdi:calendar-month"
}
},
"triggers": {
"event_ended": {
"trigger": "mdi:calendar-end"
},
"event_started": {
"trigger": "mdi:calendar-start"
}
}
}
+39 -1
View File
@@ -45,6 +45,14 @@
"title": "Detected use of deprecated action calendar.list_events"
}
},
"selector": {
"trigger_offset_type": {
"options": {
"after": "After",
"before": "Before"
}
}
},
"services": {
"create_event": {
"description": "Adds a new calendar event.",
@@ -103,5 +111,35 @@
"name": "Get events"
}
},
"title": "Calendar"
"title": "Calendar",
"triggers": {
"event_ended": {
"description": "Triggers when a calendar event ends.",
"fields": {
"offset": {
"description": "Offset from the end of the event.",
"name": "Offset"
},
"offset_type": {
"description": "Whether to trigger before or after the end of the event, if an offset is defined.",
"name": "Offset type"
}
},
"name": "Calendar event ended"
},
"event_started": {
"description": "Triggers when a calendar event starts.",
"fields": {
"offset": {
"description": "Offset from the start of the event.",
"name": "Offset"
},
"offset_type": {
"description": "Whether to trigger before or after the start of the event, if an offset is defined.",
"name": "Offset type"
}
},
"name": "Calendar event started"
}
}
}
+258 -58
View File
@@ -2,24 +2,34 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Coroutine
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import datetime
import logging
from typing import Any
from typing import TYPE_CHECKING, Any, cast
import voluptuous as vol
from homeassistant.const import CONF_ENTITY_ID, CONF_EVENT, CONF_OFFSET, CONF_PLATFORM
from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback
from homeassistant.const import (
ATTR_ENTITY_ID,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_OFFSET,
CONF_OPTIONS,
CONF_TARGET,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.automation import move_top_level_schema_fields_to_options
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import (
async_track_point_in_time,
async_track_time_interval,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.target import TargetEntityChangeTracker, TargetSelection
from homeassistant.helpers.trigger import Trigger, TriggerActionRunner, TriggerConfig
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
@@ -32,12 +42,32 @@ EVENT_START = "start"
EVENT_END = "end"
UPDATE_INTERVAL = datetime.timedelta(minutes=15)
TRIGGER_SCHEMA = cv.TRIGGER_BASE_SCHEMA.extend(
CONF_OFFSET_TYPE = "offset_type"
OFFSET_TYPE_BEFORE = "before"
OFFSET_TYPE_AFTER = "after"
_SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA = {
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}),
vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
}
_SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): DOMAIN,
vol.Required(CONF_ENTITY_ID): cv.entity_id,
vol.Optional(CONF_EVENT, default=EVENT_START): vol.In({EVENT_START, EVENT_END}),
vol.Optional(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
vol.Required(CONF_OPTIONS): _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA,
},
)
_EVENT_TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_OPTIONS, default={}): {
vol.Required(CONF_OFFSET, default=datetime.timedelta(0)): cv.time_period,
vol.Required(CONF_OFFSET_TYPE, default=OFFSET_TYPE_BEFORE): vol.In(
{OFFSET_TYPE_BEFORE, OFFSET_TYPE_AFTER}
),
},
vol.Required(CONF_TARGET): cv.TARGET_FIELDS,
}
)
@@ -50,6 +80,7 @@ class QueuedCalendarEvent:
trigger_time: datetime.datetime
event: CalendarEvent
entity_id: str
@dataclass
@@ -89,7 +120,7 @@ class Timespan:
return f"[{self.start}, {self.end})"
type EventFetcher = Callable[[Timespan], Awaitable[list[CalendarEvent]]]
type EventFetcher = Callable[[Timespan], Awaitable[list[tuple[str, CalendarEvent]]]]
type QueuedEventFetcher = Callable[[Timespan], Awaitable[list[QueuedCalendarEvent]]]
@@ -105,15 +136,24 @@ def get_entity(hass: HomeAssistant, entity_id: str) -> CalendarEntity:
return entity
def event_fetcher(hass: HomeAssistant, entity_id: str) -> EventFetcher:
def event_fetcher(hass: HomeAssistant, entity_ids: set[str]) -> EventFetcher:
"""Build an async_get_events wrapper to fetch events during a time span."""
async def async_get_events(timespan: Timespan) -> list[CalendarEvent]:
async def async_get_events(timespan: Timespan) -> list[tuple[str, CalendarEvent]]:
"""Return events active in the specified time span."""
entity = get_entity(hass, entity_id)
# Expand by one second to make the end time exclusive
end_time = timespan.end + datetime.timedelta(seconds=1)
return await entity.async_get_events(hass, timespan.start, end_time)
events: list[tuple[str, CalendarEvent]] = []
for entity_id in entity_ids:
entity = get_entity(hass, entity_id)
events.extend(
(entity_id, event)
for event in await entity.async_get_events(
hass, timespan.start, end_time
)
)
return events
return async_get_events
@@ -137,12 +177,11 @@ def queued_event_fetcher(
# Example: For an EVENT_END trigger the event may start during this
# time span, but need to be triggered later when the end happens.
results = []
for trigger_time, event in zip(
map(get_trigger_time, active_events), active_events, strict=False
):
for entity_id, event in active_events:
trigger_time = get_trigger_time(event)
if trigger_time not in offset_timespan:
continue
results.append(QueuedCalendarEvent(trigger_time + offset, event))
results.append(QueuedCalendarEvent(trigger_time + offset, event, entity_id))
_LOGGER.debug(
"Scan events @ %s%s found %s eligible of %s active",
@@ -169,14 +208,14 @@ class CalendarEventListener:
def __init__(
self,
hass: HomeAssistant,
job: HassJob[..., Coroutine[Any, Any, None] | Any],
trigger_data: dict[str, Any],
action_runner: TriggerActionRunner,
trigger_payload: dict[str, Any],
fetcher: QueuedEventFetcher,
) -> None:
"""Initialize CalendarEventListener."""
self._hass = hass
self._job = job
self._trigger_data = trigger_data
self._action_runner = action_runner
self._trigger_payload = trigger_payload
self._unsub_event: CALLBACK_TYPE | None = None
self._unsub_refresh: CALLBACK_TYPE | None = None
self._fetcher = fetcher
@@ -233,15 +272,12 @@ class CalendarEventListener:
while self._events and self._events[0].trigger_time <= now:
queued_event = self._events.pop(0)
_LOGGER.debug("Dispatching event: %s", queued_event.event)
self._hass.async_run_hass_job(
self._job,
{
"trigger": {
**self._trigger_data,
"calendar_event": queued_event.event.as_dict(),
}
},
)
payload = {
**self._trigger_payload,
ATTR_ENTITY_ID: queued_event.entity_id,
"calendar_event": queued_event.event.as_dict(),
}
self._action_runner(payload, "calendar event state change")
async def _handle_refresh(self, now_utc: datetime.datetime) -> None:
"""Handle core config update."""
@@ -259,31 +295,195 @@ class CalendarEventListener:
self._listen_next_calendar_event()
async def async_attach_trigger(
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach trigger for the specified calendar."""
entity_id = config[CONF_ENTITY_ID]
event_type = config[CONF_EVENT]
offset = config[CONF_OFFSET]
class TargetCalendarEventListener(TargetEntityChangeTracker):
"""Helper class to listen to calendar events for target entity changes."""
# Validate the entity id is valid
get_entity(hass, entity_id)
def __init__(
self,
hass: HomeAssistant,
target_selection: TargetSelection,
event_type: str,
offset: datetime.timedelta,
run_action: TriggerActionRunner,
) -> None:
"""Initialize the state change tracker."""
trigger_data = {
**trigger_info["trigger_data"],
"platform": DOMAIN,
"event": event_type,
"offset": offset,
}
listener = CalendarEventListener(
hass,
HassJob(action),
trigger_data,
queued_event_fetcher(event_fetcher(hass, entity_id), event_type, offset),
)
await listener.async_attach()
return listener.async_detach
def entity_filter(entities: set[str]) -> set[str]:
return {
entity_id
for entity_id in entities
if split_entity_id(entity_id)[0] == DOMAIN
}
super().__init__(hass, target_selection, entity_filter)
self._event_type = event_type
self._offset = offset
self._run_action = run_action
self._trigger_data = {
"event": event_type,
"offset": offset,
}
self._pending_listener_task: asyncio.Task[None] | None = None
self._calendar_event_listener: CalendarEventListener | None = None
@callback
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
"""Restart the listeners when the list of entities of the tracked targets is updated."""
if self._pending_listener_task:
self._pending_listener_task.cancel()
self._pending_listener_task = self._hass.async_create_task(
self._start_listening(tracked_entities)
)
async def _start_listening(self, tracked_entities: set[str]) -> None:
"""Start listening for calendar events."""
_LOGGER.debug("Tracking events for calendars: %s", tracked_entities)
if self._calendar_event_listener:
self._calendar_event_listener.async_detach()
self._calendar_event_listener = CalendarEventListener(
self._hass,
self._run_action,
self._trigger_data,
queued_event_fetcher(
event_fetcher(self._hass, tracked_entities),
self._event_type,
self._offset,
),
)
await self._calendar_event_listener.async_attach()
def _unsubscribe(self) -> None:
"""Unsubscribe from all events."""
super()._unsubscribe()
if self._pending_listener_task:
self._pending_listener_task.cancel()
self._pending_listener_task = None
if self._calendar_event_listener:
self._calendar_event_listener.async_detach()
self._calendar_event_listener = None
class SingleEntityEventTrigger(Trigger):
"""Legacy single calendar entity event trigger."""
_options: dict[str, Any]
@classmethod
async def async_validate_complete_config(
cls, hass: HomeAssistant, complete_config: ConfigType
) -> ConfigType:
"""Validate complete config."""
complete_config = move_top_level_schema_fields_to_options(
complete_config, _SINGLE_ENTITY_EVENT_OPTIONS_SCHEMA
)
return await super().async_validate_complete_config(hass, complete_config)
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, _SINGLE_ENTITY_EVENT_TRIGGER_SCHEMA(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.options is not None
self._options = config.options
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
entity_id = self._options[CONF_ENTITY_ID]
event_type = self._options[CONF_EVENT]
offset = self._options[CONF_OFFSET]
# Validate the entity id is valid
get_entity(self._hass, entity_id)
trigger_data = {
"event": event_type,
"offset": offset,
}
listener = CalendarEventListener(
self._hass,
run_action,
trigger_data,
queued_event_fetcher(
event_fetcher(self._hass, {entity_id}), event_type, offset
),
)
await listener.async_attach()
return listener.async_detach
class EventTrigger(Trigger):
"""Calendar event trigger."""
_options: dict[str, Any]
_event_type: str
@classmethod
async def async_validate_config(
cls, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
return cast(ConfigType, _EVENT_TRIGGER_SCHEMA(config))
def __init__(self, hass: HomeAssistant, config: TriggerConfig) -> None:
"""Initialize trigger."""
super().__init__(hass, config)
if TYPE_CHECKING:
assert config.target is not None
assert config.options is not None
self._target = config.target
self._options = config.options
async def async_attach_runner(
self, run_action: TriggerActionRunner
) -> CALLBACK_TYPE:
"""Attach a trigger."""
offset = self._options[CONF_OFFSET]
offset_type = self._options[CONF_OFFSET_TYPE]
if offset_type == OFFSET_TYPE_BEFORE:
offset = -offset
target_selection = TargetSelection(self._target)
if not target_selection.has_any_target:
raise HomeAssistantError(f"No target defined in {self._target}")
listener = TargetCalendarEventListener(
self._hass, target_selection, self._event_type, offset, run_action
)
return listener.async_setup()
class EventStartedTrigger(EventTrigger):
"""Calendar event started trigger."""
_event_type = EVENT_START
class EventEndedTrigger(EventTrigger):
"""Calendar event ended trigger."""
_event_type = EVENT_END
TRIGGERS: dict[str, type[Trigger]] = {
"_": SingleEntityEventTrigger,
"event_started": EventStartedTrigger,
"event_ended": EventEndedTrigger,
}
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
"""Return the triggers for calendars."""
return TRIGGERS
@@ -0,0 +1,27 @@
.trigger_common: &trigger_common
target:
entity:
domain: calendar
fields:
offset:
required: true
default:
days: 0
hours: 0
minutes: 0
seconds: 0
selector:
duration:
enable_day: true
offset_type:
required: true
default: before
selector:
select:
translation_key: trigger_offset_type
options:
- before
- after
event_started: *trigger_common
event_ended: *trigger_common
@@ -12,6 +12,7 @@
"codeowners": ["@emontnemery"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/cast",
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.9"],
@@ -3,6 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
import voluptuous as vol
from webexpythonsdk import ApiError, WebexAPI, exceptions
@@ -51,7 +52,7 @@ class CiscoWebexNotificationService(BaseNotificationService):
self.room = room
self.client = client
def send_message(self, message="", **kwargs):
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a message to a user."""
title = ""
@@ -5,6 +5,7 @@ from __future__ import annotations
from http import HTTPStatus
import json
import logging
from typing import Any
import requests
import voluptuous as vol
@@ -81,7 +82,7 @@ class ClicksendNotificationService(BaseNotificationService):
self.language = config[CONF_LANGUAGE]
self.voice = config[CONF_VOICE]
def send_message(self, message="", **kwargs):
def send_message(self, message: str = "", **kwargs: Any) -> None:
"""Send a voice call to a user."""
data = {
"messages": [

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