Compare commits

...

189 Commits

Author SHA1 Message Date
Franck Nijhof aad6080307 Bump omnilogic to 0.4.9 (#173938) 2026-06-16 08:51:40 +02:00
Franck Nijhof 2db2e0b0cf Bump aioairq to 0.4.8 (#173940) 2026-06-16 08:50:50 +02:00
Franck Nijhof 3fc36ab6f9 Bump messagebird to 1.2.1 (#173942) 2026-06-16 08:49:56 +02:00
Denis Shulyaka 0fad24393c Fix docs-data-update IQS for Anthropic (#173947) 2026-06-16 08:21:09 +02:00
Raphael Hehl a992a58367 Use console name in UniFi Access discovery title (#173962) 2026-06-16 08:20:29 +02:00
jasonjhofmann f0cefe2f2e Add network MAC connection to Rain Bird controller (#173672)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:15:33 +02:00
jasonjhofmann 40264992a2 Add network MAC connection to AnthemAV main zone device (#173682)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:12:52 +02:00
jasonjhofmann c29aebd60e Add network MAC connection to PlayStation 4 devices (#173681)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:11:25 +02:00
jasonjhofmann 36b74d6f05 Add network MAC connection to iAlarm device (#173676)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:02:49 +02:00
jasonjhofmann 2c626fa8f0 Add network MAC connection to Rabbit Air devices (#173684)
Co-authored-by: jasonjhofmann <16144532+jasonjhofmann@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:01:22 +02:00
jasonjhofmann cab0d015f6 Add network MAC connection to Aprilaire devices (#173675)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 08:00:13 +02:00
Erik Montnemery c544f95979 Prime condition durations from history (#173426) 2026-06-16 07:53:41 +02:00
renovate[bot] 2189d0ae74 Update infrared-protocols to 6.0.1 (#173958) 2026-06-16 07:53:22 +02:00
Raphael Hehl 9e96a06aff Bump unifi-discovery to 1.5.0 (#173927) 2026-06-16 00:06:01 +02:00
Franck Nijhof d16e0e9867 Bump greeclimate to 2.1.4 (#173924) 2026-06-15 22:53:59 +02:00
Franck Nijhof 2209996919 Bump pyipma to 3.0.10 (#173943) 2026-06-15 22:09:00 +02:00
Franck Nijhof d88767155b Bump pykrakenapi to 0.1.9 (#173933) 2026-06-15 21:47:44 +02:00
Franck Nijhof 334d02077f Bump pypck to 0.9.13 (#173914) 2026-06-15 21:46:57 +02:00
Franck Nijhof 2b7e9289d2 Bump librouteros to 3.2.1 (#173937) 2026-06-15 21:41:42 +02:00
Åke Strandberg c57358dd23 Bump pyaqvify to 0.0.10 (#173926) 2026-06-15 20:55:05 +02:00
alexborro e151478d78 Add reauthentication flow to Aquacell (#173110) 2026-06-15 20:37:03 +02:00
Mick Vleeshouwer e41b1f5279 Use device.supports_command in Overkiz (#173280) 2026-06-15 20:18:05 +02:00
LG-ThinQ-Integration 4203aed863 Add off operation_mode to SYSTEM_BOILER in LG ThinQ (#173070)
Co-authored-by: YunseonPark-LGE <yunseon.park@lge.com>
2026-06-15 20:15:08 +02:00
Nikolai Rahimi e7e116843f Raise an error when a Mitsubishi Comfort command is rejected (#173363) 2026-06-15 20:03:18 +02:00
Petro31 d781baca7e Add xy color to template lights (#173296) 2026-06-15 20:02:09 +02:00
Marcello 855962dcd0 Add reauthentication flow to Fluss+ (#173341)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:01:20 +02:00
Tomasz Dylewski cf914f559f Add battery sensor support for PAJ GPS devices (#173123) 2026-06-15 19:47:03 +02:00
Crocmagnon a420a6c990 data grand lyon: pick velo'v stop (#173407) 2026-06-15 19:46:17 +02:00
Peter Grauvogel 5f470d49a5 Add cheapest duration actions to Green Planet Energy integration (#162577)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-06-15 19:38:17 +02:00
Jason Bonta bd2638f144 Add long_press support for HomeWorks QSX in lutron_caseta (#172634)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:36:27 +02:00
Franck Nijhof b397d6fd05 Bump pygtfs to 0.1.11 (#173917) 2026-06-15 18:56:53 +02:00
Kevin Stillhammer eb2ee43e6f Remove eifinger as Broadlink codeowner (#173908) 2026-06-15 18:28:36 +02:00
Franck Nijhof 9d16e59899 Bump pykaleidescape to 1.1.6 (#173912) 2026-06-15 18:27:34 +02:00
Erik Montnemery 2434341e04 Queue nested firing of events (#173519) 2026-06-15 17:27:16 +02:00
Franck Nijhof 047edc035d Skip literal_eval for template results that cannot be a Python literal (#173664)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-15 17:24:29 +02:00
epenet 8b5f27e016 Optimize module parsing in pylint imports checker (#173077) 2026-06-15 17:12:02 +02:00
Markus Adrario 5200a8131f Homee: QS examples done (#173543) 2026-06-15 17:11:02 +02:00
Manu 2dc1870ecd Add notify entities to SMTP integration (#173557) 2026-06-15 16:32:24 +02:00
Josef Zweck d8f125dfe9 Add connectivity binary sensor to opendisplay (#172539) 2026-06-15 16:29:41 +02:00
Paulus Schoutsen 311cd56c93 Expose on-disk file path when resolving TTS media source (#172884)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-15 09:25:56 -05:00
Erwin Douna 4b17e3abcb MELCloud Home add diagnostics platform (#173583) 2026-06-15 16:24:03 +02:00
Martin Hjelmare f2839bbf7a Add util.dt.naive_now (#173443) 2026-06-15 16:22:42 +02:00
Mick Vleeshouwer 0229545184 Fix Atlantic DHW Production V2 CE FLAT C2 water heater controls in Overkiz (#172823) 2026-06-15 16:21:28 +02:00
bkobus-bbx e8ce995560 Add DHCP discovery support to BleBox integration (#173498) 2026-06-15 16:20:55 +02:00
johanzander 46ffb3bd95 Fix Growatt total_output_power 1000x too low with V1 API (#172474)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-15 16:19:44 +02:00
Åke Strandberg 27677a07a6 Aqvify reaches silver tier on quality scale (#173618) 2026-06-15 16:12:44 +02:00
Tim Laing f619ccca4b Feature/icloud media browser (#162001)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Manu <4445816+tr4nt0r@users.noreply.github.com>
2026-06-15 16:11:58 +02:00
Christian Lackas 09a72ac505 homematicip_cloud: harden post-reconnect state recovery using 2.9.0 diagnostics (#169526) 2026-06-15 16:08:40 +02:00
BrettLynch123 27573c5231 Fix daikin setup_error on transient DaikinException during startup (#173660) 2026-06-15 16:05:06 +02:00
Franck Nijhof d5f23fffa8 Bump pyvizio to 0.1.64 (#173859)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-15 16:04:18 +02:00
Paul van Schayck 3b70ac987d Migrate unifi_direct from DeviceScanner to ScannerEntity and add ConfigFlow (#171991) 2026-06-15 16:02:12 +02:00
epenet e00b8f154e Add pylint checker for direct calls to component.async_unload_entry (#173870) 2026-06-15 15:49:30 +02:00
Joost Lekkerkerker abc751fd1c Update agents to avoid useless comments (#173523)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
Co-authored-by: Franck Nijhof <git@frenck.dev>
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-06-15 14:48:02 +01:00
Franck Nijhof 6b5c7ec864 Bump pyHik to 0.4.3 (#173889) 2026-06-15 15:47:48 +02:00
Jakub Brzezowski d63bb48040 Add integration for Greencell HabuDen EVSE (#145302) 2026-06-15 15:43:53 +02:00
Åke Strandberg b71b155ffb Automatic delete of stale devices for aqvify (#173496) 2026-06-15 15:39:33 +02:00
Franck Nijhof 0f59a6070f Include spoken language in Google Generative AI STT prompt (#173631) 2026-06-15 15:39:15 +02:00
Robert Resch bb34887983 Fix aw check requirements (#173893) 2026-06-15 15:35:49 +02:00
Paul Bottein 6a06873527 Add binary sensors to Yoto (#173612) 2026-06-15 15:33:53 +02:00
epenet c012acc685 Move incorrect test from manual to demo integration (#173888) 2026-06-15 15:25:12 +02:00
Robert Resch 735ef5fc14 Add head SHA tracking to requirements check workflow (#173874) 2026-06-15 15:24:02 +02:00
epenet 405b9db101 Refactor energyid tests to avoid direct call to async_unload_entry (#173885) 2026-06-15 15:21:10 +02:00
Matt Brown 57aede0e27 Include host address in broadlink setup timeout/error messages (#173661) 2026-06-15 15:18:51 +02:00
Franck Nijhof c9d7d842ff Avoid walking script variable ChainMap twice when tracing (#173665) 2026-06-15 15:18:28 +02:00
Manu 9e8af2d098 Remove MS Teams integration (#173643) 2026-06-15 15:14:18 +02:00
EnjoyingM 90dc3717b0 Bump wolflink to 0.0.52 (#173884) 2026-06-15 15:10:05 +02:00
Paul Bottein a7c70d4d26 Add sensors to Yoto (#173292) 2026-06-15 15:05:39 +02:00
Joakim Plate 1dc5f1b768 Abort gardena discovery before product detection if address is known (#173799) 2026-06-15 14:54:54 +02:00
wollew e9f4bea715 Refactor polling rain sensor to use coordinator in Velux integration (#168991)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-06-15 14:54:22 +02:00
Neffez f2aa8aa73d Support WiZ lights with unadvertised dual head ratio (#172854) 2026-06-15 14:36:20 +02:00
Rasmus Graham 6f0831ebbb Improve Verisure session handling and reauth flow (#171317) 2026-06-15 14:35:17 +02:00
Franck Nijhof 579fbd2ae8 Add pylint checker for unnecessary format_mac in connection tuples (#173729) 2026-06-15 14:33:23 +02:00
AlCalzone e056c7d78c Deprecate openSenseMap air quality entity (#173862)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:33:05 +02:00
renovate[bot] 956dbb8757 Update pytest-aiohttp to 1.1.1 (#173828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-06-15 14:27:48 +02:00
Franck Nijhof d1ce14db17 Bump pynintendoparental to 2.4.0.1 (#173871) 2026-06-15 14:25:26 +02:00
epenet b773973ab6 Refactor inels load/unload tests (#173873) 2026-06-15 14:16:50 +02:00
Franck Nijhof c28f596740 Raise HomeAssistantError in Modern Forms action handlers (#173727) 2026-06-15 14:13:16 +02:00
Thomas D 878970a0a7 Support multi-color lights for Qbus integration (#173730) 2026-06-15 14:06:48 +02:00
some-random-climber 998746889a Use hass.config_entries.async_setup in tibber tests (#173867) 2026-06-15 14:05:05 +02:00
some-random-climber d71eb19a64 Use hass.config_entries.async_setup in vesync tests (#173868) 2026-06-15 14:04:52 +02:00
bdlcalvin 3b46bf45e7 Add oven stop button to Whirlpool (#173413) 2026-06-15 14:04:30 +02:00
starkillerOG 79d7b96b8d Use HA webhook instead of reolink_aio webhook (#173840)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-15 14:04:06 +02:00
some-random-climber 479db477e9 Use hass.config_entries.async_setup in emulated_roku tests (#173865) 2026-06-15 14:03:28 +02:00
epenet 28430a660d Speedup test_send_video in telegram_bot tests (#173861) 2026-06-15 13:57:59 +02:00
Erik Montnemery f0d5d1d526 Add tests showing races in entity triggers (#173492)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-06-15 12:58:03 +02:00
Franck Nijhof 2fd8222465 Bump fyta_cli to 0.7.3 (#173784) 2026-06-15 12:53:42 +02:00
Franck Nijhof c36dd62e2e Bump guppy3 to 3.1.7 (#173844) 2026-06-15 13:38:47 +03:00
vemboy20 2c3d629edb Update documentation status for Roborock component (#173486) 2026-06-15 12:28:55 +02:00
Brett Adams 72d72ba428 Add Powerwall 3 support to powerwall integration (#169137)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-15 12:20:13 +02:00
Franck Nijhof f9b8f05403 Bump aiopnsense to 1.0.10 (#173843) 2026-06-15 12:09:23 +02:00
Erwin Douna 6dfe6472af MELCloud Home refactor error translations (#173852) 2026-06-15 12:08:31 +02:00
some-random-climber 5ac1dd8288 Use hass.config_entries.async_setup in telegram_bot tests (#173857) 2026-06-15 12:05:35 +02:00
some-random-climber 68b2a1326c Use hass.config_entries.async_setup in iometer tests (#173855) 2026-06-15 11:57:26 +02:00
some-random-climber b56d261e53 Use hass.config_entries.async_setup in inels tests (#173854) 2026-06-15 11:57:13 +02:00
Franck Nijhof f9f37d1c2c Bump boschshcpy to 0.2.111 (#173842) 2026-06-15 11:43:37 +02:00
Franck Nijhof 0dffc84280 Bump buienradar to 1.0.9 (#173848) 2026-06-15 11:27:07 +02:00
Petro31 2ffcc70544 Add brightness_pct to set_level action (#173285) 2026-06-15 12:19:55 +03:00
some-random-climber 3c4757d944 Migrate async_setup_entry test calls to hass.config_entries.async_setup (#173850) 2026-06-15 11:02:08 +02:00
some-random-climber 1f6c45ca7a Migrate async_setup_entry test calls to hass.config_entries.async_setup (#173849) 2026-06-15 10:49:26 +02:00
Assaf Inbal efb08dfd78 Bump pyituran to 0.1.6 (#173833) 2026-06-15 10:43:55 +02:00
Franck Nijhof 65b469e185 Bump fjaraskupan to 2.3.4 (#173841) 2026-06-15 10:37:00 +02:00
Erik Montnemery 00b004c329 Make zone.async_in_zones prioritize the smallest zone (#173106)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-06-15 10:24:19 +02:00
Franck Nijhof c51409758e Fix exception translation placeholder mismatch in Teslemetry (#173734) 2026-06-15 11:08:42 +03:00
Erwin Douna 9a3d4a0ecc Add initial Proxmox quality scale (#161140)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Tom <CoMPaTech@users.noreply.github.com>
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-15 11:04:04 +03:00
Franck Nijhof 8c94cfe124 Bump switchbot-api to 2.11.1 (#173795) 2026-06-15 10:59:17 +03:00
Franck Nijhof 8290bd2d8e Bump pyipp to 0.17.2 (#173797) 2026-06-15 10:58:46 +03:00
Franck Nijhof 24045c67d2 Bump zabbix-utils to 2.0.4 (#173839) 2026-06-15 09:57:34 +02:00
Franck Nijhof 9f30cb3c4f Bump datadog to 0.52.1 (#173838) 2026-06-15 09:53:35 +02:00
Franck Nijhof 12c3bb3482 Bump rflink to 0.0.68 (#173808)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-15 09:49:50 +02:00
Franck Nijhof c164652d03 Bump debugpy to 1.8.21 (#173826)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
2026-06-15 09:39:34 +02:00
Franck Nijhof 6c483b46fb Bump pysnmp to 7.1.27 (#173791) 2026-06-15 09:29:46 +02:00
Franck Nijhof e86986bc83 Bump solax to 3.2.4 (#173787) 2026-06-15 09:17:30 +02:00
Franck Nijhof 0cffa7ba6b Bump tessie-api to 0.1.3 (#173803) 2026-06-15 09:01:15 +02:00
Franck Nijhof cff58aed7d Bump gTTS to 2.5.4 (#173836) 2026-06-15 08:58:40 +02:00
Franck Nijhof a4e89020cf Bump pyblu to 2.0.8 (#173789) 2026-06-15 08:27:09 +02:00
Franck Nijhof 143b340327 Bump aioqsw to 0.4.3 (#173802) 2026-06-15 08:26:12 +02:00
Ronald van der Meer 39fe1479e2 Fix BSCO2 sensors not being added in Duco (#173794) 2026-06-15 08:25:50 +02:00
Ronald van der Meer 07635debda Redact raw payload fields in Duco diagnostics (#173790) 2026-06-15 08:25:19 +02:00
Franck Nijhof 6313450ec1 Bump pyomie to 1.1.3 (#173824) 2026-06-15 08:24:03 +02:00
NANI 6e3643ebc2 Bump victron-vrm to 0.1.12 (#173821) 2026-06-15 08:23:42 +02:00
Franck Nijhof 5e2af44e05 Bump goslide-api to 0.7.4 (#173823) 2026-06-15 08:23:01 +02:00
Franck Nijhof 6fab59c9d2 Bump openwrt-ubus-rpc to 0.0.3 (#173810) 2026-06-15 08:20:29 +02:00
Franck Nijhof e0041c7361 Bump roborock dependencies (#173766) 2026-06-15 08:18:54 +02:00
renovate[bot] 9e2ac3e5ca Update rf-protocols to 4.3.0 (#173829) 2026-06-15 06:57:05 +02:00
Franck Nijhof 95f261db66 Bump serialx to 1.8.2 (#173817) 2026-06-15 06:27:23 +02:00
Franck Nijhof c576d5267a Bump steamloop to 1.2.1 (#173809) 2026-06-14 19:45:34 -05:00
Franck Nijhof 1241a11c9d Bump bleak-esphome to 3.9.4 (#173825) 2026-06-14 17:56:47 -05:00
Franck Nijhof 8f7447de58 Bump aiocomelit to 2.0.5 (#173800) 2026-06-15 00:00:16 +02:00
Franck Nijhof 1e54dba835 Bump pyiskra to 0.1.29 (#173820) 2026-06-15 00:00:07 +02:00
Manu 20583d6d1b Bump pyrate-limiter to 4.4.0 (#173819) 2026-06-14 23:59:57 +02:00
Franck Nijhof b94370ee51 Bump devolo-home-control-api to 0.19.1 (#173806) 2026-06-14 23:59:51 +02:00
Franck Nijhof d068a2aa11 Bump blinkpy to 0.25.6 (#173811) 2026-06-14 23:59:30 +02:00
Franck Nijhof 5d53a1f204 Bump anthemav to 1.4.2 (#173812) 2026-06-14 23:59:08 +02:00
Franck Nijhof 2908f37130 Bump pyimouapi to 1.2.8 (#173813) 2026-06-14 23:58:45 +02:00
Franck Nijhof a88a795ad3 Bump bluecurrent-api to 1.3.3 (#173815) 2026-06-14 23:58:24 +02:00
Franck Nijhof 73a36a2c47 Bump denonavr to 1.3.3 (#173814) 2026-06-14 23:58:05 +02:00
Franck Nijhof 054494181e Bump geniushub-client to 0.7.4 (#173818) 2026-06-14 23:57:38 +02:00
Franck Nijhof e4e8f901ab Bump yalexs-ble to 3.3.1 (#173792) 2026-06-14 14:58:12 -05:00
Franck Nijhof b7a29bfa2f Bump pyrainbird to 6.3.1 (#173786) 2026-06-14 21:23:59 +02:00
Franck Nijhof 26b0079945 Bump pyenphase to 2.4.9 (#173785) 2026-06-14 14:13:54 -05:00
Raphael Hehl 7454f40dd8 Bump uiprotect to 13.1.2 (#173728) 2026-06-14 19:12:07 +02:00
Franck Nijhof 26b7d1e32c Slugify OwnTracks beacon name in entity ID (#173629) 2026-06-14 19:12:05 +02:00
Franck Nijhof f7342ea9b0 Add missing translation_domain to nasweb exception raises (#173732) 2026-06-14 19:11:42 +02:00
Franck Nijhof 825d99ddaf Bump uv to 0.11.21 (#173768) 2026-06-14 19:09:43 +02:00
Michael 401fae6bdd Bump py-synologydsm-api to 2.10.0 (#173774) 2026-06-14 19:08:49 +02:00
Franck Nijhof 5433beeec1 Skip Miele fan set_percentage when already at the target step (#173725) 2026-06-14 19:06:54 +02:00
G Johansson af60e248d3 Remove listener from holiday calendar when entity is disabled (#173759) 2026-06-14 19:05:33 +02:00
Franck Nijhof 8c452c280f Add missing flow form field translation in hue (#173747) 2026-06-14 19:02:44 +02:00
Franck Nijhof 3aec970321 Add missing flow form field translations in islamic_prayer_times (#173749) 2026-06-14 19:02:03 +02:00
Franck Nijhof 687c91d5f4 Add missing flow form field translation in lacrosse_view (#173750) 2026-06-14 19:01:36 +02:00
Franck Nijhof 377fdceb6c Add missing flow form field translation in melnor (#173752) 2026-06-14 19:00:56 +02:00
Franck Nijhof 11a4533ccc Fix flow form field translation key in meteoclimatic (#173754) 2026-06-14 19:00:12 +02:00
Franck Nijhof 52b2738b2a Add missing flow form field translation in motionblinds_ble (#173758) 2026-06-14 18:59:23 +02:00
Franck Nijhof 3fda722dbb Add missing flow form field translation in blink (#173756) 2026-06-14 18:58:52 +02:00
Franck Nijhof e01215da0e Fix exception translation placeholder mismatch in Swiss Public Transport (#173735) 2026-06-14 18:58:05 +02:00
starkillerOG 6c116cf3e4 Bump reolink_aio to 0.21.1 (#173772) 2026-06-14 18:56:55 +02:00
Franck Nijhof 8017e802dd Bump aiopulse to 0.4.7 (#173763) 2026-06-14 18:56:12 +02:00
Franck Nijhof 501d956b1b Bump pylint to 4.0.6 (#173769) 2026-06-14 18:52:24 +02:00
Franck Nijhof 8aca342a78 Bump snapcast to 2.3.8 (#173765) 2026-06-14 18:51:48 +02:00
Franck Nijhof bd68e9fbe3 Bump syrupy to 5.3.2 (#173767) 2026-06-14 18:51:12 +02:00
Franck Nijhof b75c839868 Bump python-linkplay to 0.2.14 (#173770) 2026-06-14 18:49:59 +02:00
Franck Nijhof 742bfb00ff Bump anova-wifi to 0.17.1 (#173764) 2026-06-14 18:49:24 +02:00
Franck Nijhof 987c19d991 Replace duplicate SERVICE_RELOAD constant with homeassistant.const import in conversation (#173741) 2026-06-14 18:48:55 +02:00
Franck Nijhof b4319c4d0c Bump aioacaia to 0.1.18 (#173762) 2026-06-14 18:35:35 +02:00
Franck Nijhof 0fdb3ebed7 Bump aioamazondevices to 14.0.4 (#173761) 2026-06-14 18:16:06 +02:00
G Johansson efa3334616 Bump lxml to 6.1.1 (#173748) 2026-06-14 18:00:43 +02:00
Franck Nijhof 9ec0f2fe4f Add missing flow form field translation in gogogate2 (#173753) 2026-06-14 17:30:29 +02:00
Franck Nijhof 9bc5e2b06b Fix flow form field translation key in lookin (#173751) 2026-06-14 17:30:21 +02:00
Franck Nijhof 46a38cc481 Add missing flow form field translation in flux_led (#173746) 2026-06-14 17:30:13 +02:00
Franck Nijhof a63f2f1d20 Replace duplicate constants with homeassistant.const imports in Teslemetry (#173744) 2026-06-14 17:26:39 +02:00
Franck Nijhof 744bb6a068 Add missing flow form field translation in tuya (#173745) 2026-06-14 17:26:16 +02:00
Franck Nijhof d449e3e97b Replace duplicate constants with homeassistant.const imports in select (#173743) 2026-06-14 17:25:53 +02:00
Franck Nijhof 0df379704f Replace duplicate CONF_OPTIONS constant with homeassistant.const import in input_select (#173742) 2026-06-14 17:25:24 +02:00
Franck Nijhof 4ab7ce04a8 Add missing exception translation key in Israel Rail (#173738) 2026-06-14 17:24:54 +02:00
Franck Nijhof 210b08b637 Fix exception translation placeholder mismatch in Homevolt (#173737) 2026-06-14 17:24:31 +02:00
Franck Nijhof f0b448dc6e Fix exception translation placeholder mismatch in Snoo (#173736) 2026-06-14 17:24:04 +02:00
Franck Nijhof b5a314bf60 Add missing exception translation keys in VeSync (#173739) 2026-06-14 17:23:38 +02:00
Franck Nijhof 741c342749 Replace duplicate CONF_EVENT constant with homeassistant.const import in calendar (#173740) 2026-06-14 17:23:11 +02:00
Franck Nijhof f4d4df9c35 Fix options flow form field translation key in plaato (#173755) 2026-06-14 17:22:07 +02:00
Franck Nijhof bcbdf7b2bb Add missing flow form field translation in snooz (#173760) 2026-06-14 17:21:15 +02:00
iluebbe b3309ef169 Add Powerline hint to username field description (#167473)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-06-14 16:57:21 +02:00
epenet caaf5f9715 Adjust pylint checker to prevent invalid use of Platform enum (#173374)
Co-authored-by: Markus Tuominen <3738613+Markus98@users.noreply.github.com>
2026-06-14 16:17:00 +02:00
BrettLynch123 7ce7de3650 Fix tessie setup_error on transient aiohttp.ClientError during startup (#173659) 2026-06-14 15:49:22 +02:00
fdebrus 2c14c6be75 Optimistic UI updates for Vistapool write entities (#173373)
Co-authored-by: Claude <noreply@anthropic.com>
2026-06-14 15:41:57 +02:00
Christian Lackas e020f338ab Add window state sensor for HomematicIP rotary handle (HmIP-SRH) (#173423) 2026-06-14 15:36:15 +02:00
jasonjhofmann c85c2c4cd3 Add network MAC connection to JVC Projector device (#173683)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:56:29 +02:00
Raman Gupta c4e618e990 Stop validating # of slots in zwave_js.set_credential action (#173644) 2026-06-14 14:18:32 +02:00
Åke Strandberg 5efde60d21 Remove unnecessary #pylint disable..." (#173726) 2026-06-14 14:17:16 +02:00
jasonjhofmann d9dc10ed81 Add network MAC connection to myStrom bulb devices (#173707)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:16:50 +02:00
493 changed files with 17589 additions and 2077 deletions
+3 -1
View File
@@ -6,6 +6,7 @@
- Start review comments with a short, one-sentence summary of the suggested fix.
- Do not comment on code style, formatting or linting issues.
- Flag comments that over-explain straightforward code, narrate the obvious, or read like AI commentary (multi-sentence justifications for a single line).
- A Pull Request with a dependency version bump should only contain changes required for the version bump. If the PR includes other changes, request that they are removed from the PR.
# GitHub Copilot & Claude Code Instructions
@@ -50,4 +51,5 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
- When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why — non-obvious constraints, surprising behavior, or workarounds — never what.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
@@ -50,19 +50,24 @@ jobs:
check-latest: true
- name: Install script dependencies
run: pip install -r script/check_requirements/requirements.txt
- name: Collect PR diff
- name: Collect PR diff and head SHA
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
run: |
mkdir -p deterministic
gh pr diff "${PR_NUMBER}" > deterministic/pr.diff
HEAD_SHA=$(gh pr view "${PR_NUMBER}" --json headRefOid --jq '.headRefOid')
echo "head_sha=${HEAD_SHA}" >> "${GITHUB_OUTPUT}"
- name: Run deterministic checks
env:
PR_NUMBER: ${{ inputs.pull_request_number || github.event.pull_request.number }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
python -m script.check_requirements \
--pr-number "${PR_NUMBER}" \
--head-sha "${HEAD_SHA}" \
--diff deterministic/pr.diff \
--output deterministic/results.json
- name: Upload deterministic-results artifact
+76 -3
View File
@@ -1,4 +1,4 @@
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"e4fcdd04986da27ef3059faa0cea3d64bb879fe12085ebfdec0041bbc31ec181","body_hash":"3894ded07d5934ac5f29d160ffb1f9115cf72b6da8a7e453a4d4f69e8641a48e","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"328ce915f8b178bbcfcb1b69f397ac996456a4ab50490805b5b3bdd26cbf58fe","body_hash":"0665c72fe4b0a14b3a0b396dc293ce78235771fe15ebc894662fece33b189135","compiler_version":"v0.79.6","strict":true,"agent_id":"copilot","engine_versions":{"copilot":"1.0.60"}}
# gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"df4cb1c069e1874edd31b4311f1884172cec0e10","version":"v6.0.3"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e","version":"v6.4.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"v0.79.6","version":"v0.79.6"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2","digest":"sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.27.2@sha256:f88e5b17b6b7a600117bc121114d6ce2155c88c983c0c939c5df884f730fa1d6"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2","digest":"sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.27.2@sha256:ee39841d980878ebbb87592903b06d31a1af500c71525c9616f7e8e2a27041a4"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2","digest":"sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.27.2@sha256:2e3a717e5f19a654cd9a2263beb52012b56bcb68562ec5ae2e42f9d156b49591"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.3.25","digest":"sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.3.25@sha256:c10331ad17668ef89f38f5e356678788a40b0cd5fef96e8f92e1d9c1de47cbaa"},{"image":"ghcr.io/github/github-mcp-server:v1.1.2","digest":"sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c","pinned_image":"ghcr.io/github/github-mcp-server:v1.1.2@sha256:30197479d8036c7811892bc07e06f9a05c9ef3cdd79bc59f256d50647f95788c"}]}
# ___ _ _
# / _ \ | | (_)
@@ -345,6 +345,7 @@ jobs:
needs:
- activation
- extract_pr_number
- gate
if: needs.activation.outputs.daily_effective_workflow_exceeded != 'true'
runs-on: ubuntu-latest
permissions:
@@ -994,6 +995,7 @@ jobs:
- agent
- detection
- extract_pr_number
- gate
- safe_outputs
if: >
always() && (needs.agent.result != 'skipped' || needs.activation.outputs.lockdown_check_failed == 'true' ||
@@ -1428,8 +1430,8 @@ jobs:
}
extract_pr_number:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
@@ -1460,6 +1462,77 @@ jobs:
PR=$(jq -r '.pr_number' /tmp/deterministic/results.json)
echo "pr_number=${PR}" >> "${GITHUB_OUTPUT}"
gate:
needs: activation
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Configure GH_HOST for enterprise compatibility
id: ghes-host-config
shell: bash
# zizmor: ignore[github-env] - GITHUB_SERVER_URL is set by GitHub Actions, not user input.
run: |
# Derive GH_HOST from GITHUB_SERVER_URL so the gh CLI targets the correct
# GitHub instance (GHES/GHEC). On github.com this is a harmless no-op.
GH_HOST="${GITHUB_SERVER_URL#https://}"
GH_HOST="${GH_HOST#http://}"
echo "GH_HOST=${GH_HOST}" >> "$GITHUB_ENV"
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
- name: Decide whether requirements changed since the last comment
id: gate
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check comment.
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oE '<!-- requirements-check-sha: [0-9a-f]{7,40} -->' \
| grep -oE '[0-9a-f]{7,40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
pre_activation:
runs-on: ubuntu-slim
outputs:
+65 -3
View File
@@ -22,9 +22,69 @@ safe-outputs:
needs:
- extract_pr_number
jobs:
extract_pr_number:
gate:
# Skip the (token-spending) agent when no tracked requirement file changed
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: read
outputs:
skip: ${{ steps.gate.outputs.skip }}
steps:
- name: Download deterministic-results artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: check-requirements-deterministic
path: /tmp/gate
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Decide whether requirements changed since the last comment
id: gate
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
PR=$(jq -r '.pr_number' /tmp/gate/results.json)
HEAD=$(jq -r '.head_sha // empty' /tmp/gate/results.json)
if [ -z "${HEAD}" ]; then
echo "Artifact has no head_sha; running the agent."
exit 0
fi
# Recover the commit recorded in the most recent requirements-check comment.
PRIOR=$(gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR}/comments" \
--jq '.[] | select(.body | contains("<!-- requirements-check -->")) | .body' \
| grep -oE '<!-- requirements-check-sha: [0-9a-f]{7,40} -->' \
| grep -oE '[0-9a-f]{7,40}' | tail -1 || true)
if [ -z "${PRIOR}" ]; then
echo "No previous comment with a recorded commit; running the agent."
exit 0
fi
if [ "${PRIOR}" = "${HEAD}" ]; then
echo "Head ${HEAD} unchanged since the last comment; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
exit 0
fi
# List files changed between the recorded commit and the current head.
# Tracked patterns mirror script/check_requirements/diff.py TRACKED_PATTERNS.
CHANGED=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PRIOR}...${HEAD}" \
--jq '.files[].filename' 2>/dev/null) || {
echo "Could not compare ${PRIOR}...${HEAD}; running the agent."
exit 0
}
TRACKED=$(printf '%s\n' "${CHANGED}" \
| grep -Ex 'requirements.*\.txt|homeassistant/package_constraints\.txt' || true)
if [ -z "${TRACKED}" ]; then
echo "No tracked requirement files changed since ${PRIOR}; skipping the agent."
echo "skip=true" >> "${GITHUB_OUTPUT}"
else
echo "Tracked requirement files changed since ${PRIOR}; running the agent:"
printf '%s\n' "${TRACKED}"
fi
extract_pr_number:
needs: gate
if: needs.gate.outputs.skip != 'true' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
permissions:
actions: read
outputs:
@@ -128,8 +188,10 @@ Then stop. Do not improvise a verdict.
Replace every placeholder with the resolved value and emit
`rendered_comment` via `add_comment`. Preserve the leading
`<!-- requirements-check -->` marker. The PR target is already wired;
do not pass `item_number`.
`<!-- requirements-check -->` marker and the
`<!-- requirements-check-sha: … -->` marker that follows it — the next
run reads the recorded commit from it to decide whether anything changed.
The PR target is already wired; do not pass `item_number`.
## Check instructions
+2 -1
View File
@@ -40,4 +40,5 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
- Integrations with Platinum or Gold level in the Integration Quality Scale reflect a high standard of code quality and maintainability. When looking for examples of something, these are good places to start. The level is indicated in the manifest.json of the integration.
- When reviewing entity actions, do not suggest extra defensive checks for input fields that are already validated by Home Assistant's service/action schemas and entity selection filters. Suggest additional guards only when data bypasses those validators or is transformed into a less-safe form.
- When validation guarantees a dict key exists, prefer direct key access (`data["key"]`) instead of `.get("key")` so contract violations are surfaced instead of silently masked.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why — non-obvious constraints, surprising behavior, or workarounds — never what.
- Keep comments concise. Prefer one short line stating the non-obvious constraint, or no comment at all.
- Do not add comments that just restate the code on the following line(s) (e.g. `# Check if initialized` above `if self.initialized:`). Comments should only explain why (non-obvious constraints, surprising behavior, or workarounds), never what. Never add comments that justify a change by referencing what the code looked like before.
Generated
+5 -3
View File
@@ -262,8 +262,8 @@ CLAUDE.md @home-assistant/core
/tests/components/braviatv/ @bieniu @Drafteed
/homeassistant/components/bring/ @miaucl @tr4nt0r
/tests/components/bring/ @miaucl @tr4nt0r
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am @eifinger
/homeassistant/components/broadlink/ @danielhiversen @felipediel @L-I-Am
/tests/components/broadlink/ @danielhiversen @felipediel @L-I-Am
/homeassistant/components/brother/ @bieniu
/tests/components/brother/ @bieniu
/homeassistant/components/brottsplatskartan/ @gjohansson-ST
@@ -695,6 +695,8 @@ CLAUDE.md @home-assistant/core
/tests/components/gree/ @cmroche
/homeassistant/components/green_planet_energy/ @petschni
/tests/components/green_planet_energy/ @petschni
/homeassistant/components/greencell/ @BrzezowskiGC
/tests/components/greencell/ @BrzezowskiGC
/homeassistant/components/greeneye_monitor/ @jkeljo
/tests/components/greeneye_monitor/ @jkeljo
/homeassistant/components/group/ @home-assistant/core
@@ -1155,7 +1157,6 @@ CLAUDE.md @home-assistant/core
/tests/components/motionmount/ @laiho-vogels
/homeassistant/components/mqtt/ @emontnemery @jbouwh @bdraco
/tests/components/mqtt/ @emontnemery @jbouwh @bdraco
/homeassistant/components/msteams/ @peroyvind
/homeassistant/components/mta/ @OnFreund
/tests/components/mta/ @OnFreund
/homeassistant/components/mullvad/ @meichthys
@@ -1891,6 +1892,7 @@ CLAUDE.md @home-assistant/core
/homeassistant/components/unifi_access/ @imhotep @RaHehl
/tests/components/unifi_access/ @imhotep @RaHehl
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/tests/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifi_discovery/ @RaHehl
/tests/components/unifi_discovery/ @RaHehl
/homeassistant/components/unifiled/ @florisvdk
-1
View File
@@ -11,7 +11,6 @@
"microsoft_face_identify",
"microsoft_face",
"microsoft",
"msteams",
"onedrive",
"onedrive_for_business",
"xbox"
+1 -1
View File
@@ -26,5 +26,5 @@
"iot_class": "local_push",
"loggers": ["aioacaia"],
"quality_scale": "platinum",
"requirements": ["aioacaia==0.1.17"]
"requirements": ["aioacaia==0.1.18"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["serialx==1.8.0"]
"requirements": ["serialx==1.8.2"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/acmeda",
"iot_class": "local_push",
"loggers": ["aiopulse"],
"requirements": ["aiopulse==0.4.6"]
"requirements": ["aiopulse==0.4.7"]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["aioairq"],
"requirements": ["aioairq==0.4.7"],
"requirements": ["aioairq==0.4.8"],
"zeroconf": [
{
"properties": {
@@ -1,10 +1,6 @@
"""The AirVisual Pro integration."""
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -32,7 +28,7 @@ class AirVisualProEntity(CoordinatorEntity[AirVisualProCoordinator]):
connections={
(
CONNECTION_NETWORK_MAC,
format_mac(self.coordinator.data["status"]["mac_address"]),
self.coordinator.data["status"]["mac_address"],
)
},
manufacturer="AirVisual",
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==14.0.3"]
"requirements": ["aioamazondevices==14.0.4"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"]
"requirements": ["anova-wifi==0.17.1"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["anthemav"],
"requirements": ["anthemav==1.4.1"]
"requirements": ["anthemav==1.4.2"]
}
@@ -12,7 +12,7 @@ from homeassistant.components.media_player import (
)
from homeassistant.const import CONF_MAC, CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -87,9 +87,12 @@ class AnthemAVR(MediaPlayerEntity):
via_device=(DOMAIN, mac_address),
)
else:
# Zone 1 is the physical receiver that owns the network MAC; higher
# zones are via_device children and carry no connection.
self._attr_unique_id = mac_address
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac_address)},
connections={(CONNECTION_NETWORK_MAC, mac_address)},
name=name,
manufacturer=MANUFACTURER,
model=model,
@@ -52,10 +52,7 @@ rules:
status: exempt
comment: |
Service integration, no discovery.
docs-data-update:
status: exempt
comment: |
No data updates.
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
@@ -193,6 +193,7 @@ class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
connections={(dr.CONNECTION_NETWORK_MAC, data[Attribute.MAC_ADDRESS])},
name=self.create_device_name(data),
manufacturer="Aprilaire",
)
@@ -1,5 +1,6 @@
"""Config flow for Aquacell integration."""
from collections.abc import Mapping
from datetime import datetime
import logging
from typing import Any
@@ -31,6 +32,12 @@ DATA_SCHEMA = vol.Schema(
}
)
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aquacell."""
@@ -77,3 +84,48 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=DATA_SCHEMA,
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
errors: dict[str, str] = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
session = async_get_clientsession(self.hass)
api = AquacellApi(
session, reauth_entry.data.get(CONF_BRAND, Brand.AQUACELL)
)
try:
refresh_token = await api.authenticate(
reauth_entry.data[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except ApiException, TimeoutError:
errors["base"] = "cannot_connect"
except AuthenticationFailed:
errors["base"] = "invalid_auth"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_SCHEMA,
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
errors=errors,
)
@@ -14,7 +14,7 @@ from aioaquacell import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
@@ -79,7 +79,7 @@ class AquacellCoordinator(DataUpdateCoordinator[dict[str, Softener]]):
softeners = await self.aquacell_api.get_all_softeners()
except AuthenticationFailed as err:
raise ConfigEntryError from err
raise ConfigEntryAuthFailed from err
except (AquacellApiException, TimeoutError) as err:
raise UpdateFailed(f"Error communicating with API: {err}") from err
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,13 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"description": "The password for {email} is no longer valid. Enter your current softener mobile app password to reconnect.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"user": {
"data": {
"brand": "Brand",
+19 -2
View File
@@ -12,6 +12,7 @@ from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -49,6 +50,7 @@ class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
self.api_client = AqvifyAPI(
entry.data[CONF_API_KEY], websession=async_get_clientsession(hass)
)
self.previous_devices: set[str] = set()
async def _async_setup(self) -> None:
"""Set up the coordinator."""
@@ -102,10 +104,25 @@ class AqvifyCoordinator(DataUpdateCoordinator[AqvifyCoordinatorData]):
},
) from err
current_devices = set(devices.devices.keys())
if stale_devices := self.previous_devices - current_devices:
account_id = self.config_entry.unique_id
device_registry = dr.async_get(self.hass)
for device_id in stale_devices:
device = device_registry.async_get_device(
identifiers={(DOMAIN, f"{account_id}_{device_id}")}
)
if device:
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
self.previous_devices = current_devices
device_data = {}
for device in devices.devices.values():
for aqvify_device in devices.devices.values():
try:
device_key = str(device.device_key)
device_key = str(aqvify_device.device_key)
device_data[
device_key
] = await self.api_client.async_get_device_latest_data(device_key)
@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["pyaqvify"],
"quality_scale": "bronze",
"requirements": ["pyaqvify==0.0.9"]
"quality_scale": "silver",
"requirements": ["pyaqvify==0.0.10"]
}
@@ -29,16 +29,28 @@ rules:
unique-config-entry: done
# Silver
action-exceptions: todo
action-exceptions:
status: exempt
comment: |
The integration does not provide any actions.
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: todo
integration-owner: todo
log-when-unavailable: todo
docs-configuration-parameters:
status: exempt
comment: |
There are no configuration options.
docs-installation-parameters: done
entity-unavailable:
status: done
comment: |
Handled by coordinator.
integration-owner: done
log-when-unavailable:
status: done
comment: |
Handled by coordinator.
parallel-updates: done
reauthentication-flow: todo
test-coverage: todo
reauthentication-flow: done
test-coverage: done
# Gold
devices: todo
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.1"]
}
+43 -23
View File
@@ -17,6 +17,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
from . import get_maybe_authenticated_session
@@ -75,6 +76,21 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders={"address": f"{host}:{port}"},
)
async def _async_box_from_host_or_abort(
self, api_host: ApiHost
) -> Box | ConfigFlowResult:
"""Try to connect to the device; return product or an abort result."""
try:
return await Box.async_from_host(api_host)
except UnsupportedBoxVersion:
return self.async_abort(reason="unsupported_device_version")
except UnsupportedBoxResponse:
return self.async_abort(reason="unsupported_device_response")
except UnauthorizedRequest:
return self.async_abort(reason="authorization_required")
except Error:
return self.async_abort(reason="cannot_connect")
async def _async_from_host_or_form(
self, api_host: ApiHost, user_input: dict[str, Any], step_id: str
) -> tuple[Box, None] | tuple[None, ConfigFlowResult]:
@@ -101,45 +117,50 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
ex, schema, host, port, UNKNOWN, _LOGGER.error, step_id
)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
hass = self.hass
ipaddress = (discovery_info.host, discovery_info.port)
self.device_config["host"] = discovery_info.host
self.device_config["port"] = discovery_info.port
websession = async_get_clientsession(hass)
async def _async_handle_discovery(self, host: str, port: int) -> ConfigFlowResult:
"""Handle discovery by IP and port; probe device then confirm with the user."""
self.device_config["host"] = host
self.device_config["port"] = port
websession = async_get_clientsession(self.hass)
api_host = ApiHost(
*ipaddress, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER
host, port, DEFAULT_SETUP_TIMEOUT, websession, self.hass.loop, _LOGGER
)
try:
product = await Box.async_from_host(api_host)
except UnauthorizedRequest:
return self.async_abort(reason="authorization_required")
except UnsupportedBoxVersion:
return self.async_abort(reason="unsupported_device_version")
except UnsupportedBoxResponse:
return self.async_abort(reason="unsupported_device_response")
result = await self._async_box_from_host_or_abort(api_host)
if not isinstance(result, Box):
return result
product = result
self.device_config["name"] = product.name
# Check if configured but IP changed since
await self.async_set_unique_id(product.unique_id)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.host})
self._abort_if_unique_id_configured(updates={CONF_HOST: host})
self.context.update(
{
"title_placeholders": {
"name": self.device_config["name"],
"host": self.device_config["host"],
"host": host,
},
"configuration_url": f"http://{discovery_info.host}",
"configuration_url": f"http://{host}",
}
)
return await self.async_step_confirm_discovery()
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
return await self._async_handle_discovery(discovery_info.ip, DEFAULT_PORT)
async def async_step_zeroconf(
self, discovery_info: ZeroconfServiceInfo
) -> ConfigFlowResult:
"""Handle zeroconf discovery."""
return await self._async_handle_discovery(
discovery_info.host, discovery_info.port or DEFAULT_PORT
)
async def async_step_confirm_discovery(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -158,7 +179,6 @@ class BleBoxConfigFlow(ConfigFlow, domain=DOMAIN):
description_placeholders={
"name": self.device_config["name"],
"host": self.device_config["host"],
"port": self.device_config["port"],
},
)
@@ -3,6 +3,45 @@
"name": "BleBox devices",
"codeowners": ["@bbx-a", "@swistakm", "@bkobus-bbx"],
"config_flow": true,
"dhcp": [
{ "hostname": "rollergate*" },
{ "hostname": "gatebox*" },
{ "hostname": "doorbox*" },
{ "hostname": "shutterbox*" },
{ "hostname": "switchbox*" },
{ "hostname": "dimmerbox*" },
{ "hostname": "dacbox*" },
{ "hostname": "wlightbox*" },
{ "hostname": "pixelbox*" },
{ "hostname": "saunabox*" },
{ "hostname": "thermobox*" },
{ "hostname": "tempsensor*" },
{ "hostname": "energymeter*" },
{ "hostname": "airsensor*" },
{ "hostname": "humiditysensor*" },
{ "hostname": "rainsensor*" },
{ "hostname": "floodsensor*" },
{ "hostname": "luxsensor*" },
{ "hostname": "inputsensor*" },
{ "hostname": "opensensor*" },
{ "hostname": "windsensor*" },
{ "hostname": "co2sensor*" },
{ "hostname": "simongo*" },
{ "hostname": "sabaj-k-smrt*" },
{ "hostname": "rico*" },
{ "hostname": "smartrollergate*" },
{ "hostname": "darco_ero_32ws_0*" },
{ "hostname": "pergoladc*" },
{ "hostname": "seltsmartscreen*" },
{ "hostname": "seltvenetianblind*" },
{ "hostname": "doorunitbox*" },
{ "hostname": "drutexsmart*" },
{ "hostname": "swingatecontroller*" },
{ "hostname": "windowopener*" },
{ "hostname": "smartawning*" },
{ "hostname": "smartshade*" },
{ "hostname": "smartshutter*" }
],
"documentation": "https://www.home-assistant.io/integrations/blebox",
"integration_type": "device",
"iot_class": "local_polling",
@@ -4,6 +4,7 @@
"address_already_configured": "A BleBox device is already configured at {address}.",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"authorization_required": "The BleBox device requires authentication.",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
"unique_id_mismatch": "The device identifier does not match the previously configured device.",
@@ -18,6 +19,10 @@
},
"flow_title": "{name} ({host})",
"step": {
"confirm_discovery": {
"description": "Do you want to add the BleBox device **{name}** at `{host}` to Home Assistant?",
"title": "BleBox device discovered"
},
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
+1 -1
View File
@@ -21,5 +21,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.2"]
"requirements": ["blinkpy==0.25.6"]
}
@@ -26,6 +26,12 @@
"description": "The credentials for {username} need to be updated",
"title": "Re-authenticate Blink"
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
}
},
"user": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.3.2"]
"requirements": ["bluecurrent-api==1.3.3"]
}
+1 -1
View File
@@ -105,7 +105,7 @@ class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
if port == DEFAULT_PORT:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, sync_status.mac)},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["pyblu==2.0.6"],
"requirements": ["pyblu==2.0.8"],
"zeroconf": [
{
"type": "_musc._tcp.local."
@@ -118,7 +118,7 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
if port == DEFAULT_PORT:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, sync_status.mac)},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
@@ -55,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschConfigEntry) -> boo
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(shc_info.unique_id))},
connections={(dr.CONNECTION_NETWORK_MAC, shc_info.unique_id)},
identifiers={(DOMAIN, shc_info.unique_id)},
manufacturer="Bosch",
name=entry.title,
@@ -8,7 +8,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.107"],
"requirements": ["boschshcpy==0.2.111"],
"zeroconf": [
{
"name": "bosch shc*",
+8 -1
View File
@@ -118,7 +118,14 @@ class BroadlinkDevice[_ApiT: blk.Device = blk.Device]:
return False
except (NetworkTimeoutError, OSError) as err:
raise ConfigEntryNotReady from err
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="connect_failed",
translation_placeholders={
"host": api.host[0],
"error": str(err),
},
) from err
except BroadlinkException as err:
_LOGGER.error(
@@ -1,7 +1,7 @@
{
"domain": "broadlink",
"name": "Broadlink",
"codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am", "@eifinger"],
"codeowners": ["@danielhiversen", "@felipediel", "@L-I-Am"],
"config_flow": true,
"dhcp": [
{
@@ -89,6 +89,9 @@
}
},
"exceptions": {
"connect_failed": {
"message": "Failed to connect to the device at {host}: {error}"
},
"frequency_not_supported": {
"message": "Broadlink devices cannot transmit on {frequency} MHz"
},
+2 -6
View File
@@ -31,11 +31,7 @@ from homeassistant.exceptions import (
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -75,7 +71,7 @@ def get_bsblan_device_info(
"""Build DeviceInfo for the main BSB-LAN controller device."""
return DeviceInfo(
identifiers={(DOMAIN, device.MAC)},
connections={(CONNECTION_NETWORK_MAC, format_mac(device.MAC))},
connections={(CONNECTION_NETWORK_MAC, device.MAC)},
name=device.name,
manufacturer="BSBLAN Inc.",
model=(
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["buienradar", "vincenty"],
"requirements": ["buienradar==1.0.6"]
"requirements": ["buienradar==1.0.9"]
}
@@ -24,7 +24,7 @@ from homeassistant.components.websocket_api import (
ActiveConnection,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.const import CONF_EVENT, STATE_OFF, STATE_ON
from homeassistant.core import (
CALLBACK_TYPE,
HomeAssistant,
@@ -45,7 +45,6 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.json import JsonValueType
from .const import (
CONF_EVENT,
DATA_COMPONENT,
DOMAIN,
EVENT_DESCRIPTION,
@@ -13,9 +13,6 @@ if TYPE_CHECKING:
DOMAIN = "calendar"
DATA_COMPONENT: HassKey[EntityComponent[CalendarEntity]] = HassKey(DOMAIN)
# pylint: disable-next=home-assistant-duplicate-const
CONF_EVENT = "event"
class CalendarEntityFeature(IntFlag):
"""Supported features of the calendar entity."""
+1 -1
View File
@@ -460,7 +460,7 @@ class EventTrigger(Trigger):
listener = TargetCalendarEventListener(
self._hass, target_selection, self._event_type, offset, run_action
)
return listener.async_setup()
return await listener.async_setup()
class EventStartedTrigger(EventTrigger):
@@ -3,11 +3,7 @@
from cieloconnectapi.device import CieloDeviceAPI
from cieloconnectapi.model import CieloDevice
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -69,7 +65,7 @@ class CieloDeviceEntity(CieloBaseEntity):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.id)},
name=device.name,
connections={(CONNECTION_NETWORK_MAC, format_mac(device.mac_address))},
connections={(CONNECTION_NETWORK_MAC, device.mac_address)},
manufacturer="Cielo",
configuration_url="https://home.cielowigle.com/",
suggested_area=device.name,
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.3"]
"requirements": ["aiocomelit==2.0.5"]
}
@@ -8,7 +8,7 @@ from hassil.recognize import RecognizeResult
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import MATCH_ALL
from homeassistant.const import MATCH_ALL, SERVICE_RELOAD
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -53,7 +53,6 @@ from .const import (
METADATA_CUSTOM_FILE,
METADATA_CUSTOM_SENTENCE,
SERVICE_PROCESS,
SERVICE_RELOAD,
ConversationEntityFeature,
)
from .default_agent import async_setup_default_agent
@@ -19,8 +19,6 @@ ATTR_AGENT_ID = "agent_id"
ATTR_CONVERSATION_ID = "conversation_id"
SERVICE_PROCESS = "process"
# pylint: disable-next=home-assistant-duplicate-const
SERVICE_RELOAD = "reload"
DATA_COMPONENT: HassKey[EntityComponent[ConversationEntity]] = HassKey(DOMAIN)
@@ -5,6 +5,7 @@ import logging
from aiohttp import ClientConnectionError
from pydaikin.daikin_base import Appliance
from pydaikin.exceptions import DaikinException
from pydaikin.factory import DaikinFactory
from homeassistant.const import (
@@ -56,6 +57,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: DaikinConfigEntry) -> bo
except ClientConnectionError as err:
_LOGGER.debug("ClientConnectionError to %s", host)
raise ConfigEntryNotReady from err
except DaikinException as err:
# pydaikin has no subclass hierarchy for transient vs permanent errors.
# DaikinException during factory/init almost always means the device is not
# yet ready (e.g. "Empty values." when the unit hasn't finished booting),
# so treat all factory-time DaikinExceptions as transient.
_LOGGER.debug("DaikinException from %s: %s", host, err)
raise ConfigEntryNotReady from err
coordinator = DaikinCoordinator(hass, entry, device)
@@ -5,7 +5,12 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError
from data_grand_lyon_ha import DataGrandLyonClient, TclStop, find_tcl_stop_by_id
from data_grand_lyon_ha import (
DataGrandLyonClient,
TclStop,
VelovStation,
find_tcl_stop_by_id,
)
import voluptuous as vol
from homeassistant.config_entries import (
@@ -49,12 +54,6 @@ STEP_RECONFIGURE_SCHEMA = vol.Schema(
}
)
STEP_VELOV_STATION_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_STATION_ID): vol.Coerce(int),
}
)
class DataGrandLyonConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Data Grand Lyon."""
@@ -302,27 +301,96 @@ def _stop_label(stop: TclStop) -> str:
class VelovStationSubentryFlowHandler(ConfigSubentryFlow):
"""Handle a subentry flow for adding a Vélo'v station."""
def __init__(self) -> None:
"""Initialize the flow."""
self._stations: list[VelovStation] = []
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> SubentryFlowResult:
"""Handle the user step to add a new Vélo'v station."""
entry = self._get_entry()
"""Pick a station from the list fetched from the API, or enter one manually."""
if not self._stations:
if error := await self._async_load_stations():
return self.async_abort(reason=error)
errors: dict[str, str] = {}
if user_input is not None:
station_id = user_input[CONF_STATION_ID]
unique_id = f"velov_{station_id}"
try:
station_id = int(user_input[CONF_STATION_ID])
except ValueError:
errors[CONF_STATION_ID] = "invalid_station_id"
else:
entry = self._get_entry()
unique_id = f"velov_{station_id}"
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
for subentry in entry.subentries.values():
if subentry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
return self.async_create_entry(
title=f"Vélo'v {station_id}",
data={CONF_STATION_ID: station_id},
unique_id=unique_id,
return self.async_create_entry(
title=f"Vélo'v {station_id}",
data={CONF_STATION_ID: station_id},
unique_id=unique_id,
)
options = [
SelectOptionDict(
value=str(station.number), label=_velov_station_label(station)
)
for station in sorted(
self._stations,
key=lambda s: (s.name, s.commune or "", s.number or 0),
)
]
schema = vol.Schema(
{
vol.Required(CONF_STATION_ID): SelectSelector(
SelectSelectorConfig(
options=options,
mode=SelectSelectorMode.DROPDOWN,
sort=False,
custom_value=True,
)
)
}
)
return self.async_show_form(
step_id="user",
data_schema=STEP_VELOV_STATION_DATA_SCHEMA,
data_schema=schema,
errors=errors,
)
async def _async_load_stations(self) -> str | None:
"""Fetch Vélo'v stations from the API, returning an error key on failure."""
entry = self._get_entry()
session = async_get_clientsession(self.hass)
client = DataGrandLyonClient(
session=session,
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
try:
self._stations = await client.get_velov_stations()
except ClientResponseError as err:
if err.status in (401, 403):
return "invalid_auth"
return "cannot_connect"
except ClientError, TimeoutError:
return "cannot_connect"
except Exception:
_LOGGER.exception(
"Unexpected error fetching Data Grand Lyon Vélo'v stations"
)
return "unknown"
return None
def _velov_station_label(station: VelovStation) -> str:
label = station.name
if station.address or station.commune:
label += (
" (" + ", ".join(filter(None, [station.address, station.commune])) + ")"
)
label += f" - {station.number}"
return label
@@ -76,16 +76,25 @@
},
"velov_station": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"entry_type": "Vélo'v station",
"error": {
"invalid_station_id": "Station ID must be a number."
},
"initiate_flow": {
"user": "Add Vélo'v station"
},
"step": {
"user": {
"data": {
"station_id": "Station ID"
"station_id": "Station"
},
"data_description": {
"station_id": "Search by station name, address or city, or enter a station ID directly."
}
}
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_push",
"loggers": ["datadog"],
"requirements": ["datadog==0.52.0"]
"requirements": ["datadog==0.52.1"]
}
@@ -6,5 +6,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["debugpy==1.8.17"]
"requirements": ["debugpy==1.8.21"]
}
@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.3.2"],
"requirements": ["denonavr==1.3.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
@@ -9,6 +9,6 @@
"iot_class": "local_push",
"loggers": ["HomeControl", "Mydevolo", "MprmRest", "MprmWebsocket", "Mprm"],
"quality_scale": "silver",
"requirements": ["devolo-home-control-api==0.19.0"],
"requirements": ["devolo-home-control-api==0.19.1"],
"zeroconf": ["_dvl-deviceapi._tcp.local."]
}
@@ -19,11 +19,19 @@ from .coordinator import DucoConfigEntry
TO_REDACT = {
CONF_HOST,
"mac",
"Mac",
"host_name",
"HostName",
"serial_board_box",
"SerialBoardBox",
"serial_board_comm",
"SerialBoardComm",
"serial_duco_box",
"SerialDucoBox",
"serial_duco_comm",
"SerialDucoComm",
"WifiApKey",
"WifiApSsid",
}
+12 -2
View File
@@ -95,7 +95,12 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
value_fn=lambda node: node.sensor.co2 if node.sensor else None,
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
node_types=(
NodeType.BSCO2,
NodeType.UCCO2,
NodeType.VLVCO2,
NodeType.VLVCO2RH,
),
),
DucoSensorEntityDescription(
key="iaq_co2",
@@ -104,7 +109,12 @@ SENSOR_DESCRIPTIONS: tuple[DucoSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
entity_registry_enabled_default=False,
value_fn=lambda node: node.sensor.iaq_co2 if node.sensor else None,
node_types=(NodeType.UCCO2, NodeType.VLVCO2, NodeType.VLVCO2RH),
node_types=(
NodeType.BSCO2,
NodeType.UCCO2,
NodeType.VLVCO2,
NodeType.VLVCO2RH,
),
),
DucoSensorEntityDescription(
key="humidity",
+2 -8
View File
@@ -1,11 +1,7 @@
"""Base entity for the Elgato integration."""
from homeassistant.const import ATTR_CONNECTIONS, CONF_MAC
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
@@ -33,6 +29,4 @@ class ElgatoEntity(CoordinatorEntity[ElgatoDataUpdateCoordinator]):
hw_version=str(coordinator.data.info.hardware_board_type),
)
if (mac := coordinator.config_entry.data.get(CONF_MAC)) is not None:
self._attr_device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, format_mac(mac))
}
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_NETWORK_MAC, mac)}
@@ -93,7 +93,7 @@ class Enigma2UpdateCoordinator(DataUpdateCoordinator[OpenWebIfStatus]):
if "mac" in iface and iface["mac"] is not None
}
self.device_info[ATTR_CONNECTIONS] = {
(CONNECTION_NETWORK_MAC, format_mac(iface["mac"]))
(CONNECTION_NETWORK_MAC, iface["mac"])
for iface in about["info"]["ifaces"]
if "mac" in iface and iface["mac"] is not None
}
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==2.4.8"],
"requirements": ["pyenphase==2.4.9"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==45.3.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==3.9.1"
"bleak-esphome==3.9.4"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -14,5 +14,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["bleak", "fjaraskupan"],
"requirements": ["fjaraskupan==2.3.3"]
"requirements": ["fjaraskupan==2.3.4"]
}
+44 -12
View File
@@ -1,5 +1,6 @@
"""Config flow for Fluss+ integration."""
from collections.abc import Mapping
from typing import Any
from fluss_api import (
@@ -22,6 +23,21 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): cv.string})
class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Fluss+."""
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
"""Validate the API key and return any errors."""
errors: dict[str, str] = {}
client = FlussApiClient(api_key, session=async_get_clientsession(self.hass))
try:
await client.async_get_devices()
except FlussApiClientCommunicationError:
errors["base"] = "cannot_connect"
except FlussApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception occurred")
errors["base"] = "unknown"
return errors
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -31,18 +47,7 @@ class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
client = FlussApiClient(
user_input[CONF_API_KEY], session=async_get_clientsession(self.hass)
)
try:
await client.async_get_devices()
except FlussApiClientCommunicationError:
errors["base"] = "cannot_connect"
except FlussApiClientAuthenticationError:
errors["base"] = "invalid_auth"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception occurred")
errors["base"] = "unknown"
errors = await self._validate_api_key(api_key)
if not errors:
return self.async_create_entry(
title="My Fluss+ Devices", data=user_input
@@ -51,3 +56,30 @@ class FlussConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication when the API key is no longer valid."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm re-authentication with a new API key."""
errors: dict[str, str] = {}
if user_input is not None:
api_key = user_input[CONF_API_KEY]
self._async_abort_entries_match({CONF_API_KEY: api_key})
errors = await self._validate_api_key(api_key)
if not errors:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates={CONF_API_KEY: api_key},
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors,
)
@@ -12,7 +12,7 @@ from fluss_api import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@@ -60,7 +60,7 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
try:
devices = await self.api.async_get_devices()
except FlussApiClientAuthenticationError as err:
raise ConfigEntryError(f"Authentication failed: {err}") from err
raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching Fluss devices: {err}") from err
@@ -29,7 +29,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: todo
# Gold
entity-translations: done
+11 -1
View File
@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,15 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "The API key found in the profile page of the Fluss+ app."
},
"description": "The Fluss+ API key is no longer valid. Get your API key from the profile page of the Fluss+ app, or generate a new one, and enter it below."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
@@ -13,6 +13,11 @@
"discovery_confirm": {
"description": "Do you want to set up {model} {id} ({ipaddr})?"
},
"pick_device": {
"data": {
"device": "[%key:common::config_flow::data::device%]"
}
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]"
+1 -1
View File
@@ -5,7 +5,7 @@
"data_description_password": "Password for the FRITZ!Box.",
"data_description_port": "Leave empty to use the default port.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box.",
"data_description_username": "Username for the FRITZ!Box. FRITZ!Powerline devices ignore this information and accept any value.",
"data_feature_device_tracking": "Enable network device tracking"
},
"config": {
+1 -1
View File
@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["fyta_cli"],
"quality_scale": "platinum",
"requirements": ["fyta_cli==0.7.2"]
"requirements": ["fyta_cli==0.7.3"]
}
@@ -65,14 +65,16 @@ class GardenaBluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Handle the bluetooth discovery step."""
_LOGGER.debug("Discovered device: %s", discovery_info)
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
mfg = await async_get_product(self.hass, discovery_info.address)
self.devices[discovery_info.address] = mfg
if mfg.product_type not in _SUPPORTED_PRODUCT_TYPES:
return self.async_abort(reason="no_devices_found")
self.address = discovery_info.address
await self.async_set_unique_id(self.address)
self._abort_if_unique_id_configured()
return await self.async_step_confirm()
async def async_step_confirm(
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/geniushub",
"iot_class": "local_polling",
"loggers": ["geniushubclient"],
"requirements": ["geniushub-client==0.7.1"]
"requirements": ["geniushub-client==0.7.4"]
}
@@ -11,6 +11,7 @@
"step": {
"user": {
"data": {
"device": "[%key:common::config_flow::data::device%]",
"ip_address": "[%key:common::config_flow::data::ip%]",
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
@@ -230,11 +230,19 @@ class GoogleGenerativeAISttEntity(
f"audio/L{metadata.bit_rate.value};rate={metadata.sample_rate.value}",
)
prompt = self.subentry.data.get(CONF_PROMPT, DEFAULT_STT_PROMPT)
if metadata.language:
prompt = (
f"{prompt}\n"
f"The spoken language is {metadata.language}. "
f"Transcribe in that language."
)
try:
response = await self._genai_client.aio.models.generate_content(
model=self.subentry.data.get(CONF_CHAT_MODEL, RECOMMENDED_STT_MODEL),
contents=[
self.subentry.data.get(CONF_PROMPT, DEFAULT_STT_PROMPT),
prompt,
Part.from_bytes(
data=audio_data,
mime_type=f"audio/{metadata.format.value}",
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_push",
"loggers": ["gtts"],
"requirements": ["gTTS==2.5.3"]
"requirements": ["gTTS==2.5.4"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==2.1.1"]
"requirements": ["greeclimate==2.1.4"]
}
@@ -1,15 +1,132 @@
"""Green Planet Energy integration for Home Assistant."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from datetime import timedelta
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import Platform
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import GreenPlanetEnergyUpdateCoordinator
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type GreenPlanetEnergyConfigEntry = ConfigEntry[GreenPlanetEnergyUpdateCoordinator]
PLATFORMS: list[Platform] = [Platform.SENSOR]
# Service constants
SERVICE_GET_CHEAPEST_DURATION = "get_cheapest_duration"
ATTR_DURATION = "duration"
ATTR_TIME_RANGE = "time_range"
# Time range options
TIME_RANGE_DAY = "day"
TIME_RANGE_NIGHT = "night"
TIME_RANGE_FULL_DAY = "full_day"
SERVICE_GET_CHEAPEST_DURATION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_DURATION): vol.All(
vol.Coerce(float), vol.Range(min=0.5, max=24)
),
vol.Optional(ATTR_TIME_RANGE, default=TIME_RANGE_FULL_DAY): vol.In(
[TIME_RANGE_DAY, TIME_RANGE_NIGHT, TIME_RANGE_FULL_DAY]
),
}
)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Green Planet Energy component."""
async def get_cheapest_duration(call: ServiceCall) -> ServiceResponse:
"""Handle the get_cheapest_duration service call."""
# This integration has single_config_entry, so get the first entry
entries = hass.config_entries.async_entries(DOMAIN)
if not entries:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_config_entry",
)
entry = entries[0]
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="config_entry_not_loaded",
)
coordinator: GreenPlanetEnergyUpdateCoordinator = entry.runtime_data
duration = call.data[ATTR_DURATION]
time_range = call.data[ATTR_TIME_RANGE]
data = coordinator.data
api = coordinator.api
now = dt_util.now()
current_hour = now.hour
result: tuple[float | None, int | None]
if time_range == TIME_RANGE_DAY:
result = api.get_cheapest_duration_day(data, duration, current_hour)
elif time_range == TIME_RANGE_NIGHT:
result = api.get_cheapest_duration_night(data, duration, current_hour)
else: # TIME_RANGE_FULL_DAY
result = api.get_cheapest_duration(data, duration, current_hour)
avg_price, start_hour_result = result
if avg_price is None or start_hour_result is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_data_available",
)
start_time = dt_util.start_of_local_day(now).replace(
hour=start_hour_result, minute=0, second=0, microsecond=0
)
# If the calculated start time is in the past, shift to tomorrow
if start_time < now:
start_time = start_time + timedelta(days=1)
end_time = start_time + timedelta(hours=duration)
hours_until_start = (start_time - now).total_seconds() / 3600
return {
"duration": duration,
"average_price": round(avg_price / 100, 4),
"start_time": start_time.isoformat(),
"end_time": end_time.isoformat(),
"hours_until_start": round(hours_until_start, 1),
"time_range": time_range,
}
hass.services.async_register(
DOMAIN,
SERVICE_GET_CHEAPEST_DURATION,
get_cheapest_duration,
schema=SERVICE_GET_CHEAPEST_DURATION_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
return True
async def async_setup_entry(
hass: HomeAssistant, entry: GreenPlanetEnergyConfigEntry
@@ -21,6 +138,7 @@ async def async_setup_entry(
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -0,0 +1,7 @@
{
"services": {
"get_cheapest_duration": {
"service": "mdi:clock-check"
}
}
}
@@ -0,0 +1,25 @@
# Describes the format for available Green Planet Energy services
get_cheapest_duration:
fields:
duration:
required: true
example: 2.5
selector:
number:
min: 0.5
max: 24
step: 0.25
unit_of_measurement: "h"
time_range:
required: false
default: "full_day"
selector:
select:
options:
- label: Full day (00:00-24:00)
value: "full_day"
- label: Day (06:00-18:00)
value: "day"
- label: Night (18:00-06:00)
value: "night"
@@ -43,8 +43,33 @@
"api_error": {
"message": "API error: {error}"
},
"config_entry_not_loaded": {
"message": "This integration instance is not currently loaded"
},
"connection_error": {
"message": "Connection error: {error}"
},
"no_config_entry": {
"message": "No matching integration instance was found"
},
"no_data_available": {
"message": "No price data available for the requested duration and time range"
}
},
"services": {
"get_cheapest_duration": {
"description": "Retrieve electricity price data and find the cheapest consecutive time window for a given duration.",
"fields": {
"duration": {
"description": "Duration in hours for which to find the cheapest time window.",
"name": "Duration"
},
"time_range": {
"description": "Time range to search within.",
"name": "Time range"
}
},
"name": "Get cheapest duration"
}
}
}
@@ -0,0 +1,91 @@
"""Home Assistant integration for Greencell EVSE devices."""
import asyncio
from collections.abc import Callable
import json
import logging
from greencell_client.access import GreencellAccess, GreencellHaAccessLevel
from greencell_client.elec_data import ElecData3Phase, ElecDataSinglePhase
from homeassistant.components import mqtt
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from .const import CONF_SERIAL_NUMBER, DISCOVERY_TIMEOUT, GREENCELL_DISC_TOPIC
from .models import GreencellConfigEntry, GreencellRuntimeData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
def make_ready_handler(
serial: str, event: asyncio.Event
) -> Callable[[ReceiveMessage], None]:
"""Create an MQTT message handler that sets event when device matches serial."""
@callback
def _on_message(message: ReceiveMessage) -> None:
if event.is_set():
return
try:
data = json.loads(message.payload)
except ValueError, TypeError:
return
if message.topic == GREENCELL_DISC_TOPIC:
if data.get("id") != serial:
return
elif data.get("id") and data["id"] != serial:
return
event.set()
return _on_message
async def async_setup_entry(hass: HomeAssistant, entry: GreencellConfigEntry) -> bool:
"""Set up Greencell from a config entry."""
if not await mqtt.async_wait_for_mqtt_client(hass):
raise ConfigEntryNotReady("MQTT integration is not available")
serial: str = entry.data[CONF_SERIAL_NUMBER]
device_ready_event = asyncio.Event()
on_message = make_ready_handler(serial, device_ready_event)
try:
unsub_disc = await mqtt.async_subscribe(hass, GREENCELL_DISC_TOPIC, on_message)
unsub_volt = await mqtt.async_subscribe(
hass, f"/greencell/evse/{serial}/voltage", on_message
)
try:
async with asyncio.timeout(DISCOVERY_TIMEOUT):
await device_ready_event.wait()
finally:
unsub_disc()
unsub_volt()
except TimeoutError as err:
raise ConfigEntryNotReady(f"No initial data from device {serial}") from err
except HomeAssistantError as err:
raise ConfigEntryNotReady(f"MQTT error: {err}") from err
entry.runtime_data = GreencellRuntimeData(
access=GreencellAccess(GreencellHaAccessLevel.EXECUTE),
current_data=ElecData3Phase(),
voltage_data=ElecData3Phase(),
power_data=ElecDataSinglePhase(),
state_data=ElecDataSinglePhase(),
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: GreencellConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -0,0 +1,192 @@
"""Config flow for Greencell EVSE integration in Home Assistant."""
import asyncio
from collections.abc import Callable
import json
import logging
from typing import Any
from greencell_client.utils import GreencellUtils
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import mqtt
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
from . import const
from .const import (
CONF_SERIAL_NUMBER,
DOMAIN,
GREENCELL_BROADCAST_TOPIC,
GREENCELL_DISC_TOPIC,
GREENCELL_HABU_DEN,
GREENCELL_OTHER_DEVICE,
)
_LOGGER = logging.getLogger(__name__)
class EVSEConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for Greencell EVSE devices."""
VERSION = 1
MINOR_VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovered: dict[str, dict[str, Any]] = {}
self._discovered_serial: str | None = None
self._discovery_event: asyncio.Event | None = None
self._remove_listener: Callable | None = None
def _get_device_name(self, serial: str) -> str:
"""Determine the device name based on the serial number."""
return (
GREENCELL_HABU_DEN
if GreencellUtils.device_is_habu_den(serial)
else GREENCELL_OTHER_DEVICE
)
@callback
def _async_mqtt_message_received(self, msg: ReceiveMessage) -> None:
"""Handle incoming MQTT messages on the discovery topic."""
try:
payload = json.loads(msg.payload)
except json.JSONDecodeError, AttributeError:
return
serial = payload.get("id")
if isinstance(serial, str) and serial.strip():
self._discovered[serial] = payload
if self._discovery_event:
self._discovery_event.set()
async def async_step_mqtt(
self, discovery_info: MqttServiceInfo
) -> config_entries.ConfigFlowResult:
"""Handle a flow initialized by MQTT discovery."""
try:
payload = json.loads(discovery_info.payload)
serial = payload.get("id")
except json.JSONDecodeError, AttributeError:
return self.async_abort(reason="invalid_discovery_data")
if not isinstance(serial, str) or not serial.strip():
return self.async_abort(reason="invalid_discovery_data")
await self.async_set_unique_id(serial)
self._abort_if_unique_id_configured()
self._discovered_serial = serial
device_name = self._get_device_name(serial)
self.context.update({"title_placeholders": {"name": f"{device_name} {serial}"}})
return await self.async_step_confirm()
async def async_step_confirm(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Confirm addition of a discovered device."""
assert self._discovered_serial is not None
serial = self._discovered_serial
if user_input is not None:
return self.async_create_entry(
title=f"{self._get_device_name(serial)} {serial}",
data={CONF_SERIAL_NUMBER: serial},
)
return self.async_show_form(
step_id="confirm",
description_placeholders={"serial": serial},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Manual step: start active discovery process."""
try:
if not mqtt.is_connected(self.hass):
return self.async_abort(reason="mqtt_not_connected")
except KeyError:
return self.async_abort(reason="mqtt_not_configured")
return await self.async_step_discover()
async def async_step_discover(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Discovery step: subscribe, broadcast, and wait for responses."""
self._discovery_event = asyncio.Event()
try:
self._remove_listener = await mqtt.async_subscribe(
self.hass,
GREENCELL_DISC_TOPIC,
self._async_mqtt_message_received,
)
except HomeAssistantError, ValueError:
return self.async_abort(reason="mqtt_subscription_failed")
try:
payload = json.dumps({"name": "BROADCAST"})
await mqtt.async_publish(
self.hass, GREENCELL_BROADCAST_TOPIC, payload, qos=0, retain=False
)
try:
await asyncio.wait_for(
self._discovery_event.wait(), timeout=const.DISCOVERY_TIMEOUT
)
# Grace period for additional devices
await asyncio.sleep(0.5)
except TimeoutError:
_LOGGER.debug("Discovery timed out waiting for device responses")
finally:
self._remove_listener()
if not self._discovered:
return self.async_abort(reason="no_discovery_data")
if len(self._discovered) == 1:
serial = next(iter(self._discovered))
return await self._async_create_entry(serial)
return await self.async_step_select()
async def async_step_select(
self, user_input: dict[str, Any] | None = None
) -> config_entries.ConfigFlowResult:
"""Let the user select one of the discovered devices."""
if user_input is not None:
serial = user_input[CONF_SERIAL_NUMBER]
return await self._async_create_entry(serial)
return self.async_show_form(
step_id="select",
data_schema=vol.Schema(
{
vol.Required(CONF_SERIAL_NUMBER): vol.In(
list(self._discovered.keys())
)
}
),
description_placeholders={"count": str(len(self._discovered))},
)
async def _async_create_entry(self, serial: str) -> config_entries.ConfigFlowResult:
"""Finalize entry creation for selected device."""
await self.async_set_unique_id(serial, raise_on_progress=False)
self._abort_if_unique_id_configured()
device_name = self._get_device_name(serial)
title = f"{device_name} {serial}"
_LOGGER.info("Discovered and added device: %s", title)
return self.async_create_entry(
title=title,
data={CONF_SERIAL_NUMBER: serial},
)
@@ -0,0 +1,31 @@
"""Core constants for the Greencell EVSE Home Assistant integration."""
from typing import Final
# Greencell constants
DOMAIN = "greencell"
MANUFACTURER: Final = "Greencell"
# Maximal current configuration
DEFAULT_MIN_CURRENT = 6
DEFAULT_MAX_CURRENT_OTHER = 16
DEFAULT_MAX_CURRENT_HABU_DEN = 32
# Topics
GREENCELL_BROADCAST_TOPIC = "/greencell/broadcast"
GREENCELL_DISC_TOPIC = "/greencell/broadcast/device"
# Device names
GREENCELL_HABU_DEN = "Habu Den"
GREENCELL_OTHER_DEVICE = "Greencell Device"
# Other constants
DISCOVERY_MIN_TIMEOUT = 5.0
DISCOVERY_TIMEOUT = 30.0
SET_CURRENT_RETRY_TIME = 15
CONF_SERIAL_NUMBER = "serial_number"
@@ -0,0 +1,30 @@
{
"entity": {
"sensor": {
"current_l1": {
"default": "mdi:flash"
},
"current_l2": {
"default": "mdi:flash"
},
"current_l3": {
"default": "mdi:flash"
},
"power": {
"default": "mdi:battery-charging-high"
},
"status": {
"default": "mdi:ev-plug-type2"
},
"voltage_l1": {
"default": "mdi:meter-electric"
},
"voltage_l2": {
"default": "mdi:meter-electric"
},
"voltage_l3": {
"default": "mdi:meter-electric"
}
}
}
}
@@ -0,0 +1,13 @@
{
"domain": "greencell",
"name": "Greencell",
"codeowners": ["@BrzezowskiGC"],
"config_flow": true,
"dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/greencell",
"integration_type": "device",
"iot_class": "local_push",
"mqtt": ["/greencell/broadcast/device"],
"quality_scale": "bronze",
"requirements": ["greencell_client==1.0.3"]
}
@@ -0,0 +1,22 @@
"""Type definitions for Greencell integration."""
from dataclasses import dataclass
from greencell_client.access import GreencellAccess
from greencell_client.elec_data import ElecData3Phase, ElecDataSinglePhase
from homeassistant.config_entries import ConfigEntry
@dataclass
class GreencellRuntimeData:
"""Runtime data for Greencell integration."""
access: GreencellAccess
current_data: ElecData3Phase
voltage_data: ElecData3Phase
power_data: ElecDataSinglePhase
state_data: ElecDataSinglePhase
type GreencellConfigEntry = ConfigEntry[GreencellRuntimeData]
@@ -0,0 +1,63 @@
rules:
# Bronze
action-setup:
status: exempt
comment: Integration does not register custom actions or services.
appropriate-polling: done
brands: done
common-modules: done
config-flow: done
config-flow-test-coverage: done
dependency-transparency: done
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
@@ -0,0 +1,341 @@
"""Home Assistant integration module for Greencell EVSE sensor entities over MQTT."""
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any
from greencell_client.access import GreencellAccess
from greencell_client.elec_data import ElecData3Phase, ElecDataSinglePhase
from greencell_client.mqtt_parser import MqttParser
from greencell_client.utils import GreencellUtils
from homeassistant.components import mqtt
from homeassistant.components.mqtt import ReceiveMessage
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfPower,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import (
CONF_SERIAL_NUMBER,
DOMAIN,
GREENCELL_HABU_DEN,
GREENCELL_OTHER_DEVICE,
MANUFACTURER,
)
from .models import GreencellConfigEntry
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class GreencellSensorDescription(SensorEntityDescription):
"""Describe a Greencell sensor."""
value_fn: Callable[[Any], StateType]
SENSOR_DESCRIPTIONS = (
GreencellSensorDescription(
key="current_l1",
translation_key="current_l1",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=3,
value_fn=lambda data: data / 1000,
),
GreencellSensorDescription(
key="current_l2",
translation_key="current_l2",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=3,
value_fn=lambda data: data / 1000,
),
GreencellSensorDescription(
key="current_l3",
translation_key="current_l3",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
suggested_display_precision=3,
value_fn=lambda data: data / 1000,
),
GreencellSensorDescription(
key="voltage_l1",
translation_key="voltage_l1",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=2,
value_fn=lambda data: data,
),
GreencellSensorDescription(
key="voltage_l2",
translation_key="voltage_l2",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=2,
value_fn=lambda data: data,
),
GreencellSensorDescription(
key="voltage_l3",
translation_key="voltage_l3",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
suggested_display_precision=2,
value_fn=lambda data: data,
),
GreencellSensorDescription(
key="power",
translation_key="power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_display_precision=1,
value_fn=lambda data: data,
),
GreencellSensorDescription(
key="status",
translation_key="status",
device_class=SensorDeviceClass.ENUM,
options=[
"idle",
"connected",
"waiting_for_car",
"charging",
"finished",
"error_car",
"error_evse",
],
value_fn=lambda data: str(data).lower() if isinstance(data, str) else None,
),
)
# --- Config Flow Setup ---
async def async_setup_entry(
hass: HomeAssistant,
entry: GreencellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Greencell EVSE sensors from a config entry."""
serial_number: str = entry.data[CONF_SERIAL_NUMBER]
mqtt_topic_current = f"/greencell/evse/{serial_number}/current"
mqtt_topic_voltage = f"/greencell/evse/{serial_number}/voltage"
mqtt_topic_power = f"/greencell/evse/{serial_number}/power"
mqtt_topic_status = f"/greencell/evse/{serial_number}/status"
mqtt_topic_device_state = f"/greencell/evse/{serial_number}/device_state"
desc_map = {desc.key: desc for desc in SENSOR_DESCRIPTIONS}
runtime = entry.runtime_data
access = runtime.access
current_data_obj = runtime.current_data
voltage_data_obj = runtime.voltage_data
power_data_obj = runtime.power_data
state_data_obj = runtime.state_data
data_mapping = {
"current": current_data_obj,
"voltage": voltage_data_obj,
"power": power_data_obj,
"status": state_data_obj,
}
sensors: list[HabuSensor] = [
Habu3PhaseSensor(
sensor_data=data_mapping[description.key.split("_")[0]],
phase=description.key.split("_")[-1],
sensor_type=description.key,
serial_number=serial_number,
access=access,
description=description,
)
for description in SENSOR_DESCRIPTIONS
if description.key.startswith(("current_l", "voltage_l"))
]
sensors.extend(
HabuSingleSensor(
sensor_data=data_mapping[key],
serial_number=serial_number,
sensor_type=key,
access=access,
description=desc_map[key],
)
for key in ("power", "status")
)
@callback
def current_message_received(msg: ReceiveMessage) -> None:
"""Handle the current message."""
MqttParser.parse_3phase_msg(msg.payload, current_data_obj)
@callback
def voltage_message_received(msg: ReceiveMessage) -> None:
"""Handle the voltage message."""
MqttParser.parse_3phase_msg(msg.payload, voltage_data_obj)
@callback
def power_message_received(msg: ReceiveMessage) -> None:
"""Handle the power message."""
MqttParser.parse_single_phase_msg(msg.payload, "momentary", power_data_obj)
@callback
def status_message_received(msg: ReceiveMessage) -> None:
"""Handle the status message. If the device is unavailable, disable the entity."""
str_payload = (
msg.payload.decode("utf-8", errors="ignore")
if isinstance(msg.payload, (bytes, bytearray))
else str(msg.payload)
)
if "UNAVAILABLE" in str_payload or "OFFLINE" in str_payload:
access.update("UNAVAILABLE")
else:
MqttParser.parse_single_phase_msg(msg.payload, "state", state_data_obj)
@callback
def device_state_message_received(msg: ReceiveMessage) -> None:
"""Handle the device state message. If device was unavailable, enable the entity."""
access.on_msg(msg.payload)
try:
for topic, handler in (
(mqtt_topic_current, current_message_received),
(mqtt_topic_voltage, voltage_message_received),
(mqtt_topic_power, power_message_received),
(mqtt_topic_status, status_message_received),
(mqtt_topic_device_state, device_state_message_received),
):
unsub = await mqtt.async_subscribe(hass, topic, handler)
if unsub is not None:
entry.async_on_unload(unsub)
except HomeAssistantError as err:
raise ConfigEntryNotReady(f"MQTT is unavailable: {err}") from err
async_add_entities(sensors)
class HabuSensor(SensorEntity):
"""Abstract base class for Habu sensors integration."""
entity_description: GreencellSensorDescription
_attr_has_entity_name = True
_remove_listener: Callable[[], None] | None = None
def __init__(
self,
sensor_type: str,
serial_number: str,
access: GreencellAccess,
description: GreencellSensorDescription,
) -> None:
"""Initialize the sensor entity."""
self._sensor_type = sensor_type
self._serial_number = serial_number
self._access = access
self.entity_description = description
self._attr_unique_id = f"{serial_number}_{description.key}"
if GreencellUtils.device_is_habu_den(self._serial_number):
device_name = GREENCELL_HABU_DEN
else:
device_name = GREENCELL_OTHER_DEVICE
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_number)},
name=f"{device_name} {serial_number}",
manufacturer=MANUFACTURER,
model=device_name,
serial_number=serial_number,
)
@property
def available(self) -> bool:
"""Return True if the entity is available."""
return not self._access.is_disabled()
async def async_added_to_hass(self) -> None:
"""Register the entity with Home Assistant."""
unsub = self._access.register_listener(self._schedule_update)
if unsub is not None:
self.async_on_remove(unsub)
def _schedule_update(self) -> None:
"""Schedule an update for the entity."""
self.async_schedule_update_ha_state()
class Habu3PhaseSensor(HabuSensor):
"""Abstract class for 3-phase sensors (e.g. current, voltage)."""
def __init__(
self,
sensor_data: ElecData3Phase,
phase: str,
sensor_type: str,
serial_number: str,
access: GreencellAccess,
description: GreencellSensorDescription,
) -> None:
"""Initialize the 3-phase sensor."""
super().__init__(sensor_type, serial_number, access, description)
self._sensor_data = sensor_data
self._phase = phase
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
raw_value = self._sensor_data.get_value(self._phase)
if raw_value is None:
return None
return self.entity_description.value_fn(raw_value)
class HabuSingleSensor(HabuSensor):
"""Example class for sensors that return a single value."""
def __init__(
self,
sensor_data: ElecDataSinglePhase,
serial_number: str,
sensor_type: str,
access: GreencellAccess,
description: GreencellSensorDescription,
) -> None:
"""Initialize the single-value sensor."""
super().__init__(sensor_type, serial_number, access, description)
self._value = sensor_data
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
raw_value = self._value.data
if raw_value is None:
return None
return self.entity_description.value_fn(raw_value)
@@ -0,0 +1,70 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"invalid_discovery_data": "The received discovery data is invalid.",
"mqtt_not_configured": "MQTT is not configured. Please configure MQTT first.",
"mqtt_not_connected": "MQTT is not connected. Ensure the MQTT broker is running and configured.",
"mqtt_subscription_failed": "Failed to subscribe to the MQTT topic for discovery.",
"no_discovery_data": "No discovery data received. Ensure the device is online and broadcasting."
},
"step": {
"confirm": {
"description": "A Greencell device with serial number {serial} was discovered. Do you want to add it?",
"title": "Greencell device discovered"
},
"select": {
"data": {
"serial_number": "Device serial number"
},
"data_description": {
"serial_number": "Select the device you want to add to Home Assistant"
},
"description": "Multiple Greencell devices were found (total: {count}). Please choose which one you want to configure.",
"title": "Select your device"
},
"user": {
"description": "The integration will try to discover your EVSE devices over MQTT.",
"title": "Set up your Greencell HabuDen EVSE"
}
}
},
"entity": {
"sensor": {
"current_l1": {
"name": "Current phase L1"
},
"current_l2": {
"name": "Current phase L2"
},
"current_l3": {
"name": "Current phase L3"
},
"power": {
"name": "Power"
},
"status": {
"name": "Status",
"state": {
"charging": "[%key:common::state::charging%]",
"connected": "[%key:common::state::connected%]",
"error_car": "Car error",
"error_evse": "EVSE error",
"finished": "Finished",
"idle": "[%key:common::state::idle%]",
"waiting_for_car": "Waiting for car"
}
},
"voltage_l1": {
"name": "Voltage phase L1"
},
"voltage_l2": {
"name": "Voltage phase L2"
},
"voltage_l3": {
"name": "Voltage phase L3"
}
}
}
}
@@ -201,7 +201,8 @@ class GrowattCoordinator(DataUpdateCoordinator[dict[str, Any]]):
) from err
total_info["todayEnergy"] = total_info["today_energy"]
total_info["totalEnergy"] = total_info["total_energy"]
total_info["invTodayPpv"] = total_info["current_power"]
# V1 API returns current_power in kW, convert to W
total_info["invTodayPpv"] = total_info["current_power"] * 1000
else:
# Classic API: use plant_info as before.
# Copy the response to avoid mutating the dict returned by the library
+1 -1
View File
@@ -6,5 +6,5 @@
"iot_class": "local_polling",
"loggers": ["pygtfs"],
"quality_scale": "legacy",
"requirements": ["pygtfs==0.1.9"]
"requirements": ["pygtfs==0.1.11"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyhik"],
"requirements": ["pyHik==0.4.2"]
"requirements": ["pyHik==0.4.3"]
}
+1 -1
View File
@@ -47,7 +47,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HiveConfigEntry) -> bool
hub_data = devices["parent"][0]
connections: set[tuple[str, str]] = set()
if mac := hub_data.get("macAddress"):
connections.add((dr.CONNECTION_NETWORK_MAC, dr.format_mac(mac)))
connections.add((dr.CONNECTION_NETWORK_MAC, mac))
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
@@ -151,6 +151,13 @@ class HolidayCalendarEntity(CalendarEntity):
"""Set up first update."""
self._update_state_and_setup_listener()
async def async_will_remove_from_hass(self) -> None:
"""Cancel listener when removing."""
await super().async_will_remove_from_hass()
if self.unsub:
self.unsub()
self.unsub = None
def update_event(self, now: datetime) -> CalendarEvent | None:
"""Return the next upcoming event."""
next_holiday = None
+1 -3
View File
@@ -83,9 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={
(dr.CONNECTION_NETWORK_MAC, dr.format_mac(homee.settings.mac_address))
},
connections={(dr.CONNECTION_NETWORK_MAC, homee.settings.mac_address)},
identifiers={(DOMAIN, homee.settings.uid)},
manufacturer="homee",
name=homee.settings.homee_name,
@@ -48,7 +48,7 @@ rules:
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: todo
@@ -177,7 +177,7 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
"""Determine if the device is a homekit bridge or accessory."""
dev_reg = dr.async_get(self.hass)
device = dev_reg.async_get_device(
connections={(dr.CONNECTION_NETWORK_MAC, dr.format_mac(hkid))}
connections={(dr.CONNECTION_NETWORK_MAC, hkid)}
)
if device is None:
@@ -22,4 +22,7 @@ async def async_get_config_entry_diagnostics(
anonymized = handle_config(json_state, anonymize=True)
config = json.loads(anonymized)
return async_redact_data(config, TO_REDACT_CONFIG)
return {
"websocket": hap.websocket_diagnostics(),
"config": async_redact_data(config, TO_REDACT_CONFIG),
}
@@ -164,9 +164,11 @@ class HomematicipHAP:
self.set_all_to_unavailable()
elif self._ws_connection_closed.is_set():
_LOGGER.info("HMIP access point has reconnected to the cloud")
self._get_state_task = self.hass.async_create_task(self._try_get_state())
self._get_state_task.add_done_callback(self.get_state_finished)
self._ws_connection_closed.clear()
_LOGGER.debug(
"HMIP websocket diagnostics: %s",
self._websocket_diagnostic_context(),
)
self._start_get_state_task()
@callback
def async_create_entity(self, *args, **kwargs) -> None:
@@ -180,44 +182,103 @@ class HomematicipHAP:
await asyncio.sleep(30)
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
def websocket_diagnostics(self) -> dict[str, Any]:
"""Return websocket diagnostics dict (None values omitted)."""
diagnostics = {
"last_disconnect_reason": self.home.websocket_last_disconnect_reason(),
"reconnect_attempts": self.home.websocket_reconnect_attempt_count(),
"seconds_since_last_message": (
self.home.websocket_seconds_since_last_message()
),
"message_count": self.home.websocket_message_count(),
}
return {k: v for k, v in diagnostics.items() if v is not None}
def _websocket_diagnostic_context(self) -> str:
"""Return a single-line summary of websocket diagnostics for logs."""
diagnostics = self.websocket_diagnostics()
if not diagnostics:
return "no diagnostics available"
return ", ".join(f"{k}={v!r}" for k, v in diagnostics.items())
@callback
def _start_get_state_task(self) -> None:
"""Cancel any in-flight reconnect refresh and start a new one."""
if self._get_state_task is not None and not self._get_state_task.done():
_LOGGER.debug(
"Cancelling previous HomematicIP reconnect state refresh task"
)
self._get_state_task.cancel()
self._get_state_task = self.hass.async_create_task(self._try_get_state())
self._get_state_task.add_done_callback(self.get_state_finished)
self._ws_connection_closed.clear()
async def _try_get_state(self) -> None:
"""Call get_state in a loop until no error occurs.
"""Refresh state after a websocket reconnect.
Uses exponential backoff on error.
Delegates the bounded websocket wait + retry-with-exponential-backoff
to the homematicip library (``refresh_state_after_reconnect_async``),
and only handles HA-specific concerns here:
- on authentication failure, trigger reauth
- clear the per-device ``unreach`` flag and signal entity updates
(the workaround for core#160048)
"""
try:
await self.home.refresh_state_after_reconnect_async()
except HmipAuthenticationError:
_LOGGER.error(
"Authentication error from HomematicIP Cloud, triggering reauth"
)
self.config_entry.async_start_reauth(self.hass)
return
self._post_state_refresh()
# Wait until WebSocket connection is established.
while not self.home.websocket_is_connected():
await asyncio.sleep(2)
async def _on_websocket_stale(self, severity: str, seconds_since: float) -> None:
"""Log a websocket-stale event surfaced by the library.
delay = 8
max_delay = 1500
while True:
try:
await self.get_state()
break
except HmipAuthenticationError:
_LOGGER.error(
"Authentication error from HomematicIP Cloud, triggering reauth"
)
self.config_entry.async_start_reauth(self.hass)
break
except HmipConnectionError as err:
_LOGGER.warning(
"Get_state failed, retrying in %s seconds: %s", delay, err
)
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay)
The library polls staleness internally and fires this callback once
per severity per stuck period; it re-arms when fresh messages arrive.
We just translate severity to a log level.
"""
log = _LOGGER.error if severity == "error" else _LOGGER.warning
log(
"HomematicIP websocket has not received a message for "
"%.0f seconds while reporting connected",
seconds_since,
)
_LOGGER.debug(
"HMIP websocket diagnostics: %s",
self._websocket_diagnostic_context(),
)
async def get_state(self) -> None:
"""Update HMIP state and tell Home Assistant."""
await self.home.get_current_state_async()
self._post_state_refresh()
def _post_state_refresh(self) -> None:
"""Apply HA-specific post-processing after a state refresh.
``set_all_to_unavailable`` marked every device unreach=True on
disconnect; ``get_current_state_async`` only clears that flag for
devices whose state actually changed during the outage, so the rest
stay stuck unavailable after reconnect. Force-clear for all devices.
Trade-off: a device that is *genuinely* unreachable on the cloud
side will briefly appear available until its next state push
corrects it. That self-corrects, while the previous behaviour left
entities stuck unavailable indefinitely (core #160048).
"""
for device in self.home.devices:
device.unreach = False
self.update_all()
def get_state_finished(self, future) -> None:
"""Execute when try_get_state coroutine has finished."""
try:
future.result()
except asyncio.CancelledError:
_LOGGER.debug("HomematicIP reconnect state refresh task was cancelled")
except Exception as err: # noqa: BLE001
_LOGGER.error(
"Error updating state after HMIP access point reconnect: %s", err
@@ -246,6 +307,7 @@ class HomematicipHAP:
home.set_on_connected_handler(self.ws_connected_handler)
home.set_on_disconnected_handler(self.ws_disconnected_handler)
home.set_on_reconnect_handler(self.ws_reconnected_handler)
home.set_on_websocket_stale_handler(self._on_websocket_stale)
async def async_reset(self) -> bool:
"""Close the websocket connection."""
@@ -275,23 +337,28 @@ class HomematicipHAP:
"""Handle websocket connected."""
_LOGGER.info("Websocket connection to HomematicIP Cloud established")
if self._ws_connection_closed.is_set():
self._get_state_task = self.hass.async_create_task(self._try_get_state())
self._get_state_task.add_done_callback(self.get_state_finished)
self._ws_connection_closed.clear()
self._start_get_state_task()
async def ws_disconnected_handler(self) -> None:
"""Handle websocket disconnection."""
_LOGGER.warning("Websocket connection to HomematicIP Cloud closed")
_LOGGER.debug(
"HMIP websocket diagnostics: %s",
self._websocket_diagnostic_context(),
)
self._ws_connection_closed.set()
async def ws_reconnected_handler(self, reason: str) -> None:
"""Handle websocket reconnection."""
_LOGGER.info(
"Websocket connection to HomematicIP Cloud trying"
" to reconnect due to reason: %s",
"Websocket connection to HomematicIP Cloud trying to reconnect due to "
"reason: %s",
reason,
)
_LOGGER.debug(
"HMIP websocket diagnostics: %s",
self._websocket_diagnostic_context(),
)
self._ws_connection_closed.set()
@@ -27,6 +27,7 @@ from homematicip.device import (
PassageDetector,
PresenceDetectorIndoor,
RoomControlDeviceAnalog,
RotaryHandleSensor,
SmokeDetector,
SoilMoistureSensorInterface,
SwitchMeasuring,
@@ -166,6 +167,7 @@ ILLUMINATION_DEVICE_ATTRIBUTES = {
}
TILT_STATE_VALUES = ["neutral", "tilted", "non_neutral"]
WINDOW_STATE_VALUES = ["open", "closed", "tilted"]
def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
@@ -204,6 +206,9 @@ def get_device_handlers(hap: HomematicipHAP) -> dict[type, Callable]:
RoomControlDeviceAnalog: lambda device: [
HomematicipTemperatureSensor(hap, device),
],
RotaryHandleSensor: lambda device: [
HomematicipWindowStateSensor(hap, device),
],
LightSensor: lambda device: [
HomematicipIlluminanceSensor(hap, device),
],
@@ -498,6 +503,24 @@ class HomematicipTiltStateSensor(HomematicipGenericEntity, SensorEntity):
return state_attr
class HomematicipWindowStateSensor(HomematicipGenericEntity, SensorEntity):
"""Representation of the HomematicIP rotary handle window state sensor."""
_attr_device_class = SensorDeviceClass.ENUM
_attr_options = WINDOW_STATE_VALUES
_attr_translation_key = "window_state"
def __init__(self, hap: HomematicipHAP, device: RotaryHandleSensor) -> None:
"""Initialize the window state sensor."""
super().__init__(hap, device, feature_id="window_state")
@property
def native_value(self) -> str | None:
"""Return the state."""
window_state = getattr(self._device, "windowState", None)
return window_state.lower() if window_state is not None else None
class HomematicipFloorTerminalBlockMechanicChannelValve(
HomematicipGenericEntity, SensorEntity
):
@@ -98,6 +98,14 @@
"non_neutral": "Non-neutral",
"tilted": "Tilted"
}
},
"window_state": {
"name": "Window state",
"state": {
"closed": "[%key:common::state::closed%]",
"open": "[%key:common::state::open%]",
"tilted": "Tilted"
}
}
}
},

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