Compare commits

...

162 Commits

Author SHA1 Message Date
Franck Nijhof aa5c4d8786 2023.10.4 (#102397) 2023-10-21 12:00:26 +02:00
J. Nick Koston 6be401918e Bump aioesphomeapi to 18.0.7 (#102399) 2023-10-20 20:04:48 +02:00
kpine 2d6dc2bccc Fix temperature setting for multi-setpoint z-wave device (#102395)
* Fix temperature setting for multi-setpoint z-wave device

* Add missing fixture file

* Apply suggestions from code review

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-20 20:04:45 +02:00
J. Nick Koston 31daac61a9 Bump bluetooth-data-tools to 1.13.0 (#102208) 2023-10-20 20:04:42 +02:00
J. Nick Koston 1939beeca6 Bump dbus-fast to 2.12.0 (#102206) 2023-10-20 20:04:39 +02:00
J. Nick Koston 14483db892 Bump aioesphomeapi to 18.0.6 (#102195) 2023-10-20 20:04:35 +02:00
Franck Nijhof 73ac2d3dcc Bumped version to 2023.10.4 2023-10-20 17:40:08 +02:00
Álvaro Fernández Rojas 00978026cd Update aioairzone to v0.6.9 (#102383) 2023-10-20 17:39:46 +02:00
Joost Lekkerkerker c9cba77940 Bump vehicle to 2.0.0 (#102379) 2023-10-20 17:38:32 +02:00
puddly 467d24548d Bump ZHA dependencies (#102358) 2023-10-20 17:38:28 +02:00
Maikel Punie ab67304ebc Bump pyduotecno to 2023.10.1 (#102344) 2023-10-20 17:36:54 +02:00
Álvaro Fernández Rojas d55b6a0839 Handle timeouts on AEMET init (#102289)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-10-20 17:36:51 +02:00
Joost Lekkerkerker dd15e3f706 Bump aiowaqi to 2.1.0 (#102209) 2023-10-20 17:36:47 +02:00
Maikel Punie 7963008a1e Bump velbusaio to 2023.10.1 (#102178) 2023-10-20 17:36:44 +02:00
Michael Hansen 3599ddfd9f Don't warn about unknown pipeline events in ESPHome (#102174)
Don't warn about unknown events (debug)
2023-10-20 17:36:41 +02:00
iain MacDonnell f5e681ad33 Explicitly set entity name for VenstarSensor (#102158)
VenstarSensor entity name

Set entity name using _attr_name instead of a name property, to
avoid warnings about name being implicitly set to None.
2023-10-20 17:36:38 +02:00
tronikos 221efd7d4d Bump opower to 0.0.36 (#102150) 2023-10-20 17:36:34 +02:00
Jesse Hills f896b82b49 Send events for tts stream start/end (#102139) 2023-10-20 17:36:31 +02:00
Robert Svensson ec6128d9c4 Fix UniFi client tracker entities being unavailable when away on restart (#102125) 2023-10-20 17:36:28 +02:00
TheJulianJES a187f05da0 Bump zha-quirks to 0.0.105 (#102113) 2023-10-20 17:36:25 +02:00
Maikel Punie f12ce41d00 Bump velbusaio to 2023.10.0 (#102100) 2023-10-20 17:36:22 +02:00
Maikel Punie 3aba98a297 Correct sensor state attribute and device class in Velbus sensors (#102099) 2023-10-20 17:36:19 +02:00
J. Nick Koston 1a45b0af28 Bump aioesphomeapi to 18.0.3 (#102085) 2023-10-20 17:36:16 +02:00
Abílio Costa c916ac67bd Call disconnected callbacks from BT ESPHome client (#102084)
Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-20 17:36:13 +02:00
Allen Porter f7e84fcb60 Fix bug in calendar state transitions (#102083) 2023-10-20 17:36:09 +02:00
J. Nick Koston 2d7f054058 Bump aioesphomeapi to 18.0.1 (#102028)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2023-10-20 17:36:06 +02:00
J. Nick Koston 3fb79829ef Bump aioesphomeapi to 17.2.0 (#101981)
* Bump aioesphomeapi to 17.2.0

changelog: https://github.com/esphome/aioesphomeapi/compare/v17.1.5...v17.2.0

* fix import from wrong module
2023-10-20 17:36:03 +02:00
Phil Bruckner b2bbe4f4b8 Fix google_maps same last_seen bug (#101971) 2023-10-20 17:36:00 +02:00
Maximilian edce212dc9 Bump pynina to 0.3.3 (#101960) 2023-10-20 17:35:55 +02:00
Kevin Worrel 1e1dbf3cef Bump screenlogicpy to v0.9.3 (#101957) 2023-10-20 17:35:51 +02:00
Raman Gupta 24a1e540b9 Update zwave issue repair strings (#101940) 2023-10-20 17:35:46 +02:00
Vadym Holoveichuk 1371a03d14 Fix Setpoint in Matter climate platform (#101929)
fix matter max setpoint
2023-10-20 17:35:43 +02:00
J. Nick Koston 1b83620213 Bump aioesphomeapi to 17.1.5 (#101916) 2023-10-20 17:35:35 +02:00
J. Nick Koston 08d5d5336a Bump aioesphomeapi to 17.1.4 (#101897) 2023-10-20 17:35:29 +02:00
Jan Bouwhuis 406e58df69 Fix error handling on subscribe when mqtt is not initialized (#101832) 2023-10-20 17:29:45 +02:00
Archomeda e9e677d933 Fix Spotify media position update value (#100044)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2023-10-20 17:29:34 +02:00
Franck Nijhof 93d7ff3779 2023.10.3 (#101930) 2023-10-13 13:01:05 +02:00
Franck Nijhof 8b8df2ec3b Bumped version to 2023.10.3 2023-10-13 11:17:45 +02:00
J. Nick Koston 2fac26c6c6 Fix implicit device name in wiz switch (#101914) 2023-10-13 11:17:30 +02:00
starkillerOG e17a25ca4a Bump reolink-aio to 0.7.11 (#101886) 2023-10-13 11:16:44 +02:00
starkillerOG e3122ec6dc Uncancel task when swallowing CancelledError (#101884) 2023-10-13 11:16:41 +02:00
G Johansson 497da016af Add missing fan mode in Sensibo (#101883)
* Add missing fan mode in Sensibo

* translations
2023-10-13 11:16:37 +02:00
Franck Nijhof a36115993c Downgrade aiohttp to 3.8.5 (#101913) 2023-10-13 10:20:12 +02:00
Franck Nijhof f5b5215247 2023.10.2 (#101871) 2023-10-12 15:26:56 +02:00
Franck Nijhof 014546c75e Bumped version to 2023.10.2 2023-10-12 13:34:26 +02:00
Joost Lekkerkerker b0dabfa3f7 Only reload Withings config entry on reauth (#101638)
* Only reload on reauth

* Reload if entry is loaded

* Make async_cloudhook_generate_url protected

* Fix feedback
2023-10-12 13:33:54 +02:00
Betacart ca1d6ddbb6 Fix typo in remember the milk strings (#101869)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2023-10-12 13:32:45 +02:00
Martin Hjelmare 7670b5d3b0 Fix mysensors battery level attribute (#101868) 2023-10-12 13:32:39 +02:00
René Klomp 04dc44c069 Fix SMA incorrect device class (#101866) 2023-10-12 13:32:36 +02:00
Joost Lekkerkerker c2cf497302 Fix translation key in Plugwise (#101862)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-12 13:32:31 +02:00
Justin Lindh 34693d4a9b Bump Python-MyQ to v3.1.13 (#101852) 2023-10-12 13:31:46 +02:00
Brandon Rothweiler c9b9851605 Remove Mazda integration (#101849)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2023-10-12 13:26:43 +02:00
Michael Hansen 3b13c9129a Close existing UDP server for ESPHome voice assistant (#101845) 2023-10-12 13:26:39 +02:00
Michael Hansen ffe60102fd Dynamic wake word loading for Wyoming (#101827)
* Change supported_wake_words property to async method

* Add test

* Add timeout + test

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2023-10-12 13:26:34 +02:00
Álvaro Fernández Rojas 5946681454 Update aioqsw to v0.3.5 (#101809) 2023-10-12 13:26:31 +02:00
Kevin Worrel eae6f9b0f8 Await set value function in ScreenLogic number entities (#101802) 2023-10-12 13:26:27 +02:00
Michael Davie 959d21a576 Bump env_canada to 0.6.0 (#101798) 2023-10-12 13:26:20 +02:00
Richard Kroegel 785df0c8e1 Bump bimmer_connected to 0.14.1 (#101789)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2023-10-12 13:26:16 +02:00
Nathan Spencer 62805aed2b Bump pyweatherflowudp to 1.4.5 (#101770) 2023-10-12 13:26:12 +02:00
Hessel 1a2c9fd9a9 Change BiDirectional Prefix (#101764) 2023-10-12 13:26:09 +02:00
Joost Lekkerkerker f0a1977d2e Subscribe to Withings webhooks outside of coordinator (#101759)
* Subscribe to Withings webhooks outside of coordinator

* Subscribe to Withings webhooks outside of coordinator

* Update homeassistant/components/withings/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/withings/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2023-10-12 13:26:03 +02:00
Maikel Punie 8b3fc107df Bump pyDuotecno to 2023.10.0 (#101754) 2023-10-12 13:25:59 +02:00
Betacart 417ba3644b Fix typo in Ombi translation strings (#101747)
Update strings.json

Fixed typo ("Sumbit" -> "Submit")
2023-10-12 13:25:56 +02:00
Kevin Worrel 49f060d95b Bump screenlogicpy to 0.9.2 (#101746) 2023-10-12 13:25:52 +02:00
Álvaro Fernández Rojas ed57d0beac Fix Airzone climate double setpoint (#101744)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-10-12 13:25:49 +02:00
Allen Porter d7a36cb6a4 Add google calendar required feature for create event service (#101741)
* Add google calendar required feature for create event service

* Update docstring
2023-10-12 13:25:44 +02:00
Brandon Rothweiler 887263d80e Update eufylife-ble-client to 0.1.8 (#101727) 2023-10-12 13:25:40 +02:00
Robert Hillis c4737e4423 Fix Slack type error for file upload (#101720)
Fix regression
2023-10-12 13:25:37 +02:00
Jan Bouwhuis 327e6d2362 Fix mqtt sensor or binary_sensor state not saved after expiry (#101670)
Fix mqtt sensor state not saved after expire
2023-10-12 13:25:33 +02:00
Joost Lekkerkerker e32044f884 Abort config flow when invalid token is received (#101642) 2023-10-12 13:25:28 +02:00
Allen Porter dbc3382dfb Add additional calendar state alarm debugging (#101631) 2023-10-12 13:20:57 +02:00
Matthew Donoughe a042703dd7 Update pylutron-caseta to 0.18.3 (#101630) 2023-10-12 13:20:53 +02:00
Aidan Timson 8109c77f6a Bump systembridgeconnector to 3.8.4 (#101621)
Update systembridgeconnector to 3.8.4
2023-10-12 13:20:49 +02:00
J. Nick Koston 2639602f5b Fix compiling missing statistics losing rows (#101616) 2023-10-12 13:20:46 +02:00
Allen Porter d5c26beb91 Additional fix for rainbird unique id (#101599)
Additiona fix for rainbird unique id
2023-10-12 13:20:42 +02:00
Marc Mueller f24843f211 Update aiohttp to 3.8.6 (#101590) 2023-10-12 13:20:39 +02:00
Joakim Plate 5f0bf4e2a3 Update ha-philipsjs to 3.1.1 (#101574)
Update philips to 3.1.1
2023-10-12 13:20:35 +02:00
Matthias Alphart bab524f264 Update pyfronius to 0.7.2 (#101571) 2023-10-12 13:20:32 +02:00
Abílio Costa ede7d13c1e Improve Ikea Idasen config flow error messages (#101567) 2023-10-12 13:20:29 +02:00
TJ Horner db91e9a720 Auto-fix common key entry issues during WeatherKit config flow (#101504) 2023-10-12 13:20:25 +02:00
Greg Dowling c11dd58c1d Improve handling of roon media players with fixed and incremental volume (#99819) 2023-10-12 13:20:20 +02:00
Franck Nijhof a6edfa85b1 2023.10.1 (#101547) 2023-10-06 20:36:37 +02:00
Franck Nijhof b3080ae005 Bumped version to 2023.10.1 2023-10-06 19:38:00 +02:00
Joost Lekkerkerker 5925b6b912 Only import color extractor when domain is in config (#101522) 2023-10-06 19:37:44 +02:00
Joost Lekkerkerker 42b53c6349 Add Withings webhooks after a slight delay (#101542) 2023-10-06 19:37:18 +02:00
J. Nick Koston d26b1b370a Bump HAP-python to 4.8.0 (#101538) 2023-10-06 19:37:14 +02:00
Joost Lekkerkerker 7369ae8c9f Cancel callbacks on Withings entry unload (#101536) 2023-10-06 19:37:11 +02:00
Mike Woudenberg 6c2d1e2142 Update LoqedAPI to handle invalid transitions better (#101534) 2023-10-06 19:37:08 +02:00
jan iversen 4a5b0222ab Modbus, wrong length when reading strings (#101529) 2023-10-06 19:37:05 +02:00
Joost Lekkerkerker 76f78e249b Delete existing Withings cloudhook (#101527) 2023-10-06 19:37:02 +02:00
Joost Lekkerkerker 81f582eeb7 Use config flow in color extractor tests (#101524) 2023-10-06 19:36:58 +02:00
Kevin Stillhammer 7f6506cfcf Limit waze_travel_time to 0.5call/s over all entries (#101514) 2023-10-06 19:35:48 +02:00
Allen Porter d14934861e Fix for rainbird unique id (#101512) 2023-10-06 19:35:44 +02:00
Allen Porter d469626855 Fix bug in calendar state where alarms due to alarms not scheduled (#101510) 2023-10-06 19:35:41 +02:00
Ian 9725a0daf9 Fix key error in config flow when duplicate stop names exist (#101491) 2023-10-06 19:35:38 +02:00
J. Nick Koston 26c7ba38d0 Fix caching of latest short term stats after insertion of external stats (#101490) 2023-10-06 19:35:35 +02:00
Kevin Stillhammer 948bbdd2bf bump pywaze to 0.5.1 sets timeout to 60s (#101487) 2023-10-06 19:35:32 +02:00
J. Nick Koston eadc70ede0 Bump zeroconf to 0.115.2 (#101482) 2023-10-06 19:35:28 +02:00
Paul Bottein 2210db4ca6 Update frontend to 20231005.0 (#101480) 2023-10-06 19:35:25 +02:00
G Johansson c5585b0706 Fix Trafikverket Camera if no location data (#101463) 2023-10-06 19:35:22 +02:00
René Klomp 37cfa5efb7 SMA add missing entity descriptions (#101462) 2023-10-06 19:35:19 +02:00
Fredrik Erlandsson a506ba94d1 Fix device_class.capitalize() in Point (#101440) 2023-10-06 19:35:16 +02:00
Michael Davie f8c7d502df Bump env_canada to v0.5.37 (#101435) 2023-10-06 19:35:12 +02:00
Nathan Spencer f0cb2ba005 Adjust WeatherFlow wind sensors to appropriately match native unit and library field (#101418) 2023-10-06 19:35:10 +02:00
Michael Hansen f7ab00a8bf Add wake word cooldown to avoid duplicate wake-ups (#101417) 2023-10-06 19:35:06 +02:00
Joost Lekkerkerker e8c38fe99e Add translation for Tamper binary sensor (#101416) 2023-10-06 19:35:03 +02:00
J. Nick Koston a4f0da8286 Bump dbus-fast to 2.11.1 (#101406)
changelog: https://github.com/Bluetooth-Devices/dbus-fast/compare/v2.11.0...v2.11.1
2023-10-06 19:34:54 +02:00
Jan Bouwhuis 223f3a434b Raise vol.Invalid for invalid mqtt device_tracker config (#101399)
Raise vol.Invalid for invalid mqtt device_tracker
2023-10-06 19:31:58 +02:00
Joost Lekkerkerker 10e43048bd Fix Withings translations (#101397) 2023-10-06 19:31:55 +02:00
Marty Sun 2345a2be5f Bump pyyardian to 1.1.1 (#101363)
* Update Yardian Dependency

* test requirements
2023-10-06 19:31:52 +02:00
TheJulianJES 7dfb397aef Fix ZHA device diagnostics error for unknown unsupported attributes (#101239)
* Modify test to account for scenario of unknown unsupported attributes

* Add error checking for finding unsupported attributes

* Change comment to clarify zigpy misses an attribute def

This should make it more clear that it's about an unknown attribute (where zigpy doesn't have an attribute definition).

* Increase test coverage

This increases test coverage by doing the following:
- adding the `IasZone` to our test device, so we have a cluster which actually has some attribute definitions
- adding not just an unknown unsupported attribute by id, but also by name
- adding a known unsupported attribute by id and by name

* Fix diagnostics logic
2023-10-06 19:31:48 +02:00
Franck Nijhof 22bf1a0582 2023.10.0 (#101386) 2023-10-04 16:03:55 +02:00
Franck Nijhof 01daae69ab Bumped version to 2023.10.0 2023-10-04 13:48:40 +02:00
Franck Nijhof 512b2af13c Bumped version to 2023.10.0b9 2023-10-04 10:24:20 +02:00
Franck Nijhof 8e05df2b44 Bumped version to 2023.10.0b8 2023-10-04 10:11:43 +02:00
Franck Nijhof 0470ca3e76 Update Pillow to 10.0.1 (#101368) 2023-10-04 10:11:33 +02:00
Guido Schmitz ebde9914f2 Increase update interval of update platform in devolo_home_network (#101366)
Increase update interval of firmware platform
2023-10-04 10:11:30 +02:00
Luke Lashley 337f9197bb Check that dock error status is not None for Roborock (#101321)
Co-authored-by: Robert Resch <robert@resch.dev>
2023-10-04 10:11:27 +02:00
Brett Adams 9b9a16e9c6 Fix temperature when myZone is in use for Advantage air (#101316) 2023-10-04 10:11:21 +02:00
Paulus Schoutsen 55ff8e1fcb Bumped version to 2023.10.0b7 2023-10-03 22:07:38 -04:00
Jesse Hills 937a26117c Allow esphome device to disable vad on stream (#101352) 2023-10-03 22:03:20 -04:00
Jesse Hills 776b26de3f Fix manual stopping of the voice assistant pipeline (#101351) 2023-10-03 22:03:19 -04:00
Michael Hansen b9a929e63b Pipeline runs are only equal with same id (#101341)
* Pipeline runs are only equal with same id

* Use dict instead of list in PipelineRuns

* Let it blow up

* Test

* Test rest of __eq__
2023-10-03 22:03:18 -04:00
Michael Hansen 9c5d9344e2 Increase pipeline timeout to 5 minutes (#101327) 2023-10-03 22:03:17 -04:00
Michael 38423ad6f1 Bump pyW800rf32 to 0.4 (#101317)
bump pyW800rf32 from 0.3 to 0.4
2023-10-03 22:03:16 -04:00
Brett Adams 9e4f9a88ad Fix reference error in Aussie Broadband (#101315) 2023-10-03 22:03:15 -04:00
Brett Adams e0cbbf7d57 Revert PR #99077 for Aussie Broadband (#101314) 2023-10-03 22:03:14 -04:00
Aaron Collins fd6eb61489 Remove duplicated device before daikin migration (#99900)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-10-03 22:03:13 -04:00
Franck Nijhof a9bc380c32 Bumped version to 2023.10.0b6 2023-10-02 23:03:02 +02:00
Franck Nijhof be32db70a0 Update Lokalise CLI to v2.6.8 (#101297) 2023-10-02 23:02:53 +02:00
Bram Kragten 0e29ccf069 Update frontend to 20231002.0 (#101294) 2023-10-02 23:02:49 +02:00
Franck Nijhof 9834c1de9a Bumped version to 2023.10.0b5 2023-10-02 21:34:50 +02:00
Nathan Spencer 791293ca87 Bump pylitterbot to 2023.4.9 (#101285) 2023-10-02 21:34:30 +02:00
Joost Lekkerkerker 98d7945521 Revert "Use shorthand attributes in Telldus live" (#101281) 2023-10-02 21:34:26 +02:00
Michael Hansen 06d6122663 Bump intents to 2023.10.2 (#101277) 2023-10-02 21:34:23 +02:00
Luke Lashley bad9b1c95f Bump python-myq to 3.1.11 (#101266) 2023-10-02 21:34:20 +02:00
Franck Nijhof a0e5f016e1 Add documentation URL for the Home Assistant Green (#101263) 2023-10-02 21:34:17 +02:00
Joost Lekkerkerker 5370db4a3e Bump aiowaqi to 2.0.0 (#101259) 2023-10-02 21:34:14 +02:00
Erik Montnemery b069f92d95 Add missing device class to sensor.DEVICE_CLASS_UNITS (#101256) 2023-10-02 21:34:11 +02:00
Joost Lekkerkerker 9b810dcf9f Downgrade pylitterbot to 2023.4.5 (#101255) 2023-10-02 21:34:08 +02:00
Jc2k 3cfae48577 Add extra validation in private_ble_device config flow (#101254) 2023-10-02 21:34:05 +02:00
Erik Montnemery ad53ff037e Fix color temperature setting in broadlink light (#101251) 2023-10-02 21:34:02 +02:00
Erik Montnemery 63b5ba6b3a Remove dead code from broadlink light (#101063) 2023-10-02 21:33:58 +02:00
Jesse Hills e76396b184 ESPHome: fix voice assistant default audio settings (#101241) 2023-10-02 21:23:12 +02:00
Joost Lekkerkerker ebf8061117 Fix withings webhook name (#101221) 2023-10-02 21:23:09 +02:00
Kevin Stillhammer ced616fafa Remove invalid doc about multi origin/dest in google_travel_time (#101215) 2023-10-02 21:23:06 +02:00
J. Nick Koston 18f3fb42c9 Bump aioesphomeapi to 17.0.1 (#101214) 2023-10-02 21:23:03 +02:00
J. Nick Koston bfe16e2536 Bump zeroconf to 0.115.1 (#101213) 2023-10-02 21:23:00 +02:00
Aidan Timson 98ca71fc96 Split get users into chunks of 100 for Twitch sensors (#101211)
* Split get users into chunks of 100

* Move to own function
2023-10-02 21:22:57 +02:00
Luke Lashley 93033e037d Bump python-roborock to 0.34.6 (#101147) 2023-10-02 21:22:51 +02:00
Simon Lamon cfa923252b Fix loop in progress config flow (#97229)
* Fix data entry flow with multiple steps

* Update a test

* Update description and add a show progress change test

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-02 21:20:40 +02:00
Franck Nijhof 8c84237e6b Bumped version to 2023.10.0b4 2023-10-01 18:32:38 +02:00
Joost Lekkerkerker cf6f0cf266 Correct JSONDecodeError in co2signal (#101206) 2023-10-01 18:32:29 +02:00
Joost Lekkerkerker b24f09b47e Add config entry name to Withings webhook name (#101205) 2023-10-01 18:32:26 +02:00
Daniel Hjelseth Høyer e0d7c1440b Update Mill library to 0.11.6 (#101180) 2023-10-01 18:32:23 +02:00
Allen Porter b20f9c40be Clear calendar alarms after scheduling and add debug loggging (#101176) 2023-10-01 18:32:18 +02:00
Oliver b27097808d Update denonavr to 0.11.4 (#101169) 2023-10-01 18:32:15 +02:00
Allen Porter d7fa98454b Fix rainbird entity unique ids (#101168)
* Fix unique ids for rainbird entities

* Update entity unique id use based on config entry entity id

* Update tests/components/rainbird/test_binary_sensor.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Rename all entity_registry variables

* Shorten long comment under line length limits

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-10-01 18:32:12 +02:00
Tereza Tomcova 531479bf5b Bump PySwitchbot to 0.40.1 (#101164) 2023-10-01 18:32:09 +02:00
Raman Gupta 3941d2c897 Bump zwave-js-server-python to 0.52.1 (#101162) 2023-10-01 18:32:06 +02:00
hlyi c4d85ac41f Report unavailability for yolink sensor and binary_sensor (#100743) 2023-10-01 18:32:03 +02:00
c0ffeeca7 5106907571 Terminology: Rename Multi-PAN to Multiprotocol to be consistent (#99262) 2023-10-01 18:31:59 +02:00
256 changed files with 7307 additions and 4355 deletions
-2
View File
@@ -738,8 +738,6 @@ build.json @home-assistant/supervisor
/tests/components/matrix/ @PaarthShah
/homeassistant/components/matter/ @home-assistant/matter
/tests/components/matter/ @home-assistant/matter
/homeassistant/components/mazda/ @bdr99
/tests/components/mazda/ @bdr99
/homeassistant/components/meater/ @Sotolotl @emontnemery
/tests/components/meater/ @Sotolotl @emontnemery
/homeassistant/components/medcom_ble/ @elafargue
@@ -125,6 +125,13 @@ class AdvantageAirAC(AdvantageAirAcEntity, ClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the current target temperature."""
# If the system is in MyZone mode, and a zone is set, return that temperature instead.
if (
self._ac["myZone"] > 0
and not self._ac.get(ADVANTAGE_AIR_MYAUTO_ENABLED)
and not self._ac.get(ADVANTAGE_AIR_MYTEMP_ENABLED)
):
return self._myzone["setTemp"]
return self._ac["setTemp"]
@property
@@ -62,6 +62,12 @@ class AdvantageAirAcEntity(AdvantageAirEntity):
def _ac(self) -> dict[str, Any]:
return self.coordinator.data["aircons"][self.ac_key]["info"]
@property
def _myzone(self) -> dict[str, Any]:
return self.coordinator.data["aircons"][self.ac_key]["zones"].get(
f"z{self._ac['myZone']:02}"
)
class AdvantageAirZoneEntity(AdvantageAirAcEntity):
"""Parent class for Advantage Air Zone Entities."""
@@ -1,5 +1,6 @@
"""The AEMET OpenData component."""
import asyncio
import logging
from aemet_opendata.exceptions import TownNotFound
@@ -8,6 +9,7 @@ from aemet_opendata.interface import AEMET, ConnectionOptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from .const import (
@@ -37,6 +39,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except TownNotFound as err:
_LOGGER.error(err)
return False
except asyncio.TimeoutError as err:
raise ConfigEntryNotReady("AEMET OpenData API timed out") from err
weather_coordinator = WeatherUpdateCoordinator(hass, aemet)
await weather_coordinator.async_config_entry_first_refresh()
+5 -5
View File
@@ -217,8 +217,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
if ATTR_TEMPERATURE in kwargs:
params[API_SET_POINT] = kwargs[ATTR_TEMPERATURE]
if ATTR_TARGET_TEMP_LOW in kwargs and ATTR_TARGET_TEMP_HIGH in kwargs:
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
params[API_COOL_SET_POINT] = kwargs[ATTR_TARGET_TEMP_HIGH]
params[API_HEAT_SET_POINT] = kwargs[ATTR_TARGET_TEMP_LOW]
await self._async_update_hvac_params(params)
@callback
@@ -248,8 +248,8 @@ class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
self._attr_fan_mode = self._speeds.get(self.get_airzone_value(AZD_SPEED))
if self.supported_features & ClimateEntityFeature.TARGET_TEMPERATURE_RANGE:
self._attr_target_temperature_high = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_COOL_TEMP_SET
)
self._attr_target_temperature_low = self.get_airzone_value(
AZD_HEAT_TEMP_SET
)
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.6.8"]
"requirements": ["aioairzone==0.6.9"]
}
@@ -9,7 +9,7 @@ from homeassistant.components import stt
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers.typing import ConfigType
from .const import DATA_CONFIG, DOMAIN
from .const import CONF_DEBUG_RECORDING_DIR, DATA_CONFIG, DOMAIN
from .error import PipelineNotFound
from .pipeline import (
AudioSettings,
@@ -45,7 +45,9 @@ __all__ = (
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Optional("debug_recording_dir"): str},
{
vol.Optional(CONF_DEBUG_RECORDING_DIR): str,
},
)
},
extra=vol.ALLOW_EXTRA,
@@ -2,3 +2,12 @@
DOMAIN = "assist_pipeline"
DATA_CONFIG = f"{DOMAIN}.config"
DEFAULT_PIPELINE_TIMEOUT = 60 * 5 # seconds
DEFAULT_WAKE_WORD_TIMEOUT = 3 # seconds
CONF_DEBUG_RECORDING_DIR = "debug_recording_dir"
DATA_LAST_WAKE_UP = f"{DOMAIN}.last_wake_up"
DEFAULT_WAKE_WORD_COOLDOWN = 2 # seconds
@@ -3,7 +3,7 @@ from __future__ import annotations
import array
import asyncio
from collections import deque
from collections import defaultdict, deque
from collections.abc import AsyncGenerator, AsyncIterable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from enum import StrEnum
@@ -48,7 +48,13 @@ from homeassistant.util import (
)
from homeassistant.util.limited_size_dict import LimitedSizeDict
from .const import DATA_CONFIG, DOMAIN
from .const import (
CONF_DEBUG_RECORDING_DIR,
DATA_CONFIG,
DATA_LAST_WAKE_UP,
DEFAULT_WAKE_WORD_COOLDOWN,
DOMAIN,
)
from .error import (
IntentRecognitionError,
PipelineError,
@@ -399,6 +405,9 @@ class WakeWordSettings:
audio_seconds_to_buffer: float = 0
"""Seconds of audio to buffer before detection and forward to STT."""
cooldown_seconds: float = DEFAULT_WAKE_WORD_COOLDOWN
"""Seconds after a wake word detection where other detections are ignored."""
@dataclass(frozen=True)
class AudioSettings:
@@ -475,7 +484,7 @@ class PipelineRun:
stt_provider: stt.SpeechToTextEntity | stt.Provider = field(init=False, repr=False)
tts_engine: str = field(init=False, repr=False)
tts_options: dict | None = field(init=False, default=None)
wake_word_entity_id: str = field(init=False, repr=False)
wake_word_entity_id: str | None = field(init=False, default=None, repr=False)
wake_word_entity: wake_word.WakeWordDetectionEntity = field(init=False, repr=False)
abort_wake_word_detection: bool = field(init=False, default=False)
@@ -518,6 +527,13 @@ class PipelineRun:
self.audio_settings.noise_suppression_level,
)
def __eq__(self, other: Any) -> bool:
"""Compare pipeline runs by id."""
if isinstance(other, PipelineRun):
return self.id == other.id
return False
@callback
def process_event(self, event: PipelineEvent) -> None:
"""Log an event and call listener."""
@@ -596,6 +612,8 @@ class PipelineRun:
)
)
wake_word_settings = self.wake_word_settings or WakeWordSettings()
# Remove language since it doesn't apply to wake words yet
metadata_dict.pop("language", None)
@@ -605,6 +623,7 @@ class PipelineRun:
{
"entity_id": self.wake_word_entity_id,
"metadata": metadata_dict,
"timeout": wake_word_settings.timeout or 0,
},
)
)
@@ -612,8 +631,6 @@ class PipelineRun:
if self.debug_recording_queue is not None:
self.debug_recording_queue.put_nowait(f"00_wake-{self.wake_word_entity_id}")
wake_word_settings = self.wake_word_settings or WakeWordSettings()
wake_word_vad: VoiceActivityTimeout | None = None
if (wake_word_settings.timeout is not None) and (
wake_word_settings.timeout > 0
@@ -663,6 +680,17 @@ class PipelineRun:
if result is None:
wake_word_output: dict[str, Any] = {}
else:
# Avoid duplicate detections by checking cooldown
last_wake_up = self.hass.data.get(DATA_LAST_WAKE_UP)
if last_wake_up is not None:
sec_since_last_wake_up = time.monotonic() - last_wake_up
if sec_since_last_wake_up < wake_word_settings.cooldown_seconds:
_LOGGER.debug("Duplicate wake word detection occurred")
raise WakeWordDetectionAborted
# Record last wake up time to block duplicate detections
self.hass.data[DATA_LAST_WAKE_UP] = time.monotonic()
if result.queued_audio:
# Add audio that was pending at detection.
#
@@ -1025,7 +1053,7 @@ class PipelineRun:
# Directory to save audio for each pipeline run.
# Configured in YAML for assist_pipeline.
if debug_recording_dir := self.hass.data[DATA_CONFIG].get(
"debug_recording_dir"
CONF_DEBUG_RECORDING_DIR
):
if device_id is None:
# <debug_recording_dir>/<pipeline.name>/<run.id>
@@ -1565,21 +1593,19 @@ class PipelineRuns:
def __init__(self, pipeline_store: PipelineStorageCollection) -> None:
"""Initialize."""
self._pipeline_runs: dict[str, list[PipelineRun]] = {}
self._pipeline_runs: dict[str, dict[str, PipelineRun]] = defaultdict(dict)
self._pipeline_store = pipeline_store
pipeline_store.async_add_listener(self._change_listener)
def add_run(self, pipeline_run: PipelineRun) -> None:
"""Add pipeline run."""
pipeline_id = pipeline_run.pipeline.id
if pipeline_id not in self._pipeline_runs:
self._pipeline_runs[pipeline_id] = []
self._pipeline_runs[pipeline_id].append(pipeline_run)
self._pipeline_runs[pipeline_id][pipeline_run.id] = pipeline_run
def remove_run(self, pipeline_run: PipelineRun) -> None:
"""Remove pipeline run."""
pipeline_id = pipeline_run.pipeline.id
self._pipeline_runs[pipeline_id].remove(pipeline_run)
self._pipeline_runs[pipeline_id].pop(pipeline_run.id)
async def _change_listener(
self, change_type: str, item_id: str, change: dict
@@ -1589,7 +1615,7 @@ class PipelineRuns:
return
if pipeline_runs := self._pipeline_runs.get(item_id):
# Create a temporary list in case the list is modified while we iterate
for pipeline_run in list(pipeline_runs):
for pipeline_run in list(pipeline_runs.values()):
pipeline_run.abort_wake_word_detection = True
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.util import language as language_util
from .const import DOMAIN
from .const import DEFAULT_PIPELINE_TIMEOUT, DEFAULT_WAKE_WORD_TIMEOUT, DOMAIN
from .error import PipelineNotFound
from .pipeline import (
AudioSettings,
@@ -30,9 +30,6 @@ from .pipeline import (
async_get_pipeline,
)
DEFAULT_TIMEOUT = 30
DEFAULT_WAKE_WORD_TIMEOUT = 3
_LOGGER = logging.getLogger(__name__)
@@ -117,7 +114,7 @@ async def websocket_run(
)
return
timeout = msg.get("timeout", DEFAULT_TIMEOUT)
timeout = msg.get("timeout", DEFAULT_PIPELINE_TIMEOUT)
start_stage = PipelineStage(msg["start_stage"])
end_stage = PipelineStage(msg["end_stage"])
handler_id: int | None = None
@@ -3,11 +3,10 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Any
from aiohttp import ClientError
from aussiebb.asyncio import AussieBB
from aussiebb.const import FETCH_TYPES, NBN_TYPES, PHONE_TYPES
from aussiebb.const import FETCH_TYPES
from aussiebb.exceptions import AuthenticationException, UnrecognisedServiceType
from homeassistant.config_entries import ConfigEntry
@@ -23,19 +22,6 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR]
# Backport for the pyaussiebb=0.0.15 validate_service_type method
def validate_service_type(service: dict[str, Any]) -> None:
"""Check the service types against known types."""
if "type" not in service:
raise ValueError("Field 'type' not found in service data")
if service["type"] not in NBN_TYPES + PHONE_TYPES + ["Hardware"]:
raise UnrecognisedServiceType(
f"Service type {service['type']=} {service['name']=} - not recognised - ",
"please report this at https://github.com/yaleman/aussiebb/issues/new",
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Aussie Broadband from a config entry."""
# Login to the Aussie Broadband API and retrieve the current service list
@@ -44,9 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.data[CONF_PASSWORD],
async_get_clientsession(hass),
)
# Overwrite the pyaussiebb=0.0.15 validate_service_type method with backport
# Required until pydantic 2.x is supported
client.validate_service_type = validate_service_type
try:
await client.login()
services = await client.get_services(drop_types=FETCH_TYPES)
@@ -61,10 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
return await client.get_usage(service_id)
except UnrecognisedServiceType as err:
raise UpdateFailed(
f"Service {service_id} of type '{services[service_id]['type']}' was"
" unrecognised"
) from err
raise UpdateFailed(f"Service {service_id} was unrecognised") from err
return async_update_data
@@ -286,6 +286,13 @@
"on": "[%key:component::binary_sensor::entity_component::gas::state::on%]"
}
},
"tamper": {
"name": "Tamper",
"state": {
"off": "[%key:component::binary_sensor::entity_component::gas::state::off%]",
"on": "Tampering detected"
}
},
"update": {
"name": "Update",
"state": {
@@ -18,7 +18,7 @@
"bleak-retry-connector==3.2.1",
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.12.0",
"dbus-fast==2.11.0"
"bluetooth-data-tools==1.13.0",
"dbus-fast==2.12.0"
]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"iot_class": "cloud_polling",
"loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected==0.14.0"]
"requirements": ["bimmer-connected==0.14.1"]
}
+7 -16
View File
@@ -6,8 +6,7 @@ from broadlink.exceptions import BroadlinkException
from homeassistant.components.light import (
ATTR_BRIGHTNESS,
ATTR_COLOR_MODE,
ATTR_COLOR_TEMP,
ATTR_COLOR_TEMP_KELVIN,
ATTR_HS_COLOR,
ColorMode,
LightEntity,
@@ -46,6 +45,8 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
_attr_has_entity_name = True
_attr_name = None
_attr_min_color_temp_kelvin = 2700
_attr_max_color_temp_kelvin = 6500
def __init__(self, device):
"""Initialize the light."""
@@ -80,7 +81,7 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
self._attr_hs_color = [data["hue"], data["saturation"]]
if "colortemp" in data:
self._attr_color_temp = round((data["colortemp"] - 2700) / 100 + 153)
self._attr_color_temp_kelvin = data["colortemp"]
if "bulb_colormode" in data:
if data["bulb_colormode"] == BROADLINK_COLOR_MODE_RGB:
@@ -108,21 +109,11 @@ class BroadlinkLight(BroadlinkEntity, LightEntity):
state["saturation"] = int(hs_color[1])
state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB
elif ATTR_COLOR_TEMP in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP]
state["colortemp"] = (color_temp - 153) * 100 + 2700
elif ATTR_COLOR_TEMP_KELVIN in kwargs:
color_temp = kwargs[ATTR_COLOR_TEMP_KELVIN]
state["colortemp"] = color_temp
state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE
elif ATTR_COLOR_MODE in kwargs:
color_mode = kwargs[ATTR_COLOR_MODE]
if color_mode == ColorMode.HS:
state["bulb_colormode"] = BROADLINK_COLOR_MODE_RGB
elif color_mode == ColorMode.COLOR_TEMP:
state["bulb_colormode"] = BROADLINK_COLOR_MODE_WHITE
else:
# Scenes are not yet supported.
state["bulb_colormode"] = BROADLINK_COLOR_MODE_SCENES
await self._async_set_state(state)
async def async_turn_off(self, **kwargs: Any) -> None:
+20 -5
View File
@@ -483,7 +483,7 @@ class CalendarEntity(Entity):
_entity_component_unrecorded_attributes = frozenset({"description"})
_alarm_unsubs: list[CALLBACK_TYPE] = []
_alarm_unsubs: list[CALLBACK_TYPE] | None = None
@property
def event(self) -> CalendarEvent | None:
@@ -528,19 +528,26 @@ class CalendarEntity(Entity):
the current or upcoming event.
"""
super().async_write_ha_state()
if self._alarm_unsubs is None:
self._alarm_unsubs = []
_LOGGER.debug(
"Clearing %s alarms (%s)", self.entity_id, len(self._alarm_unsubs)
)
for unsub in self._alarm_unsubs:
unsub()
self._alarm_unsubs.clear()
now = dt_util.now()
event = self.event
if event is None or now >= event.end_datetime_local:
_LOGGER.debug("No alarms needed for %s (event=%s)", self.entity_id, event)
return
@callback
def update(_: datetime.datetime) -> None:
"""Run when the active or upcoming event starts or ends."""
self._async_write_ha_state()
"""Update state and reschedule next alarms."""
_LOGGER.debug("Running %s update", self.entity_id)
self.async_write_ha_state()
if now < event.start_datetime_local:
self._alarm_unsubs.append(
@@ -553,14 +560,22 @@ class CalendarEntity(Entity):
self._alarm_unsubs.append(
async_track_point_in_time(self.hass, update, event.end_datetime_local)
)
_LOGGER.debug(
"Scheduled %d updates for %s (%s, %s)",
len(self._alarm_unsubs),
self.entity_id,
event.start_datetime_local,
event.end_datetime_local,
)
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass.
To be extended by integrations.
"""
for unsub in self._alarm_unsubs:
for unsub in self._alarm_unsubs or ():
unsub()
self._alarm_unsubs = None
async def async_get_events(
self,
@@ -3,11 +3,11 @@ from __future__ import annotations
from collections.abc import Mapping
from datetime import timedelta
from json import JSONDecodeError
import logging
from typing import Any, cast
import CO2Signal
from requests.exceptions import JSONDecodeError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
@@ -24,7 +24,10 @@ from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
CONFIG_SCHEMA = vol.Schema(
{vol.Optional(DOMAIN): {}},
extra=vol.ALLOW_EXTRA,
)
# Extend the existing light.turn_on service schema
SERVICE_SCHEMA = vol.All(
@@ -62,11 +65,12 @@ def _get_color(file_handler) -> tuple:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Color extractor component."""
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
if DOMAIN in config:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data={}
)
)
)
return True
@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.9.22"]
"requirements": ["hassil==1.2.5", "home-assistant-intents==2023.10.2"]
}
+27 -4
View File
@@ -135,9 +135,11 @@ async def async_migrate_unique_id(
) -> None:
"""Migrate old entry."""
dev_reg = dr.async_get(hass)
ent_reg = er.async_get(hass)
old_unique_id = config_entry.unique_id
new_unique_id = api.device.mac
new_name = api.device.values.get("name")
new_mac = dr.format_mac(new_unique_id)
new_name = api.name
@callback
def _update_unique_id(entity_entry: er.RegistryEntry) -> dict[str, str] | None:
@@ -147,15 +149,36 @@ async def async_migrate_unique_id(
if new_unique_id == old_unique_id:
return
duplicate = dev_reg.async_get_device(
connections={(CONNECTION_NETWORK_MAC, new_mac)}, identifiers=None
)
# Remove duplicated device
if duplicate is not None:
if config_entry.entry_id in duplicate.config_entries:
_LOGGER.debug(
"Removing duplicated device %s",
duplicate.name,
)
# The automatic cleanup in entity registry is scheduled as a task, remove
# the entities manually to avoid unique_id collision when the entities
# are migrated.
duplicate_entities = er.async_entries_for_device(
ent_reg, duplicate.id, True
)
for entity in duplicate_entities:
ent_reg.async_remove(entity.entity_id)
dev_reg.async_remove_device(duplicate.id)
# Migrate devices
for device_entry in dr.async_entries_for_config_entry(
dev_reg, config_entry.entry_id
):
for connection in device_entry.connections:
if connection[1] == old_unique_id:
new_connections = {
(CONNECTION_NETWORK_MAC, dr.format_mac(new_unique_id))
}
new_connections = {(CONNECTION_NETWORK_MAC, new_mac)}
_LOGGER.debug(
"Migrating device %s connections to %s",
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==0.11.3"],
"requirements": ["denonavr==0.11.4"],
"ssdp": [
{
"manufacturer": "Denon",
@@ -8,7 +8,15 @@ import logging
from typing import Any, Concatenate, ParamSpec, TypeVar
from denonavr import DenonAVR
from denonavr.const import POWER_ON, STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
from denonavr.const import (
ALL_TELNET_EVENTS,
ALL_ZONES,
POWER_ON,
STATE_OFF,
STATE_ON,
STATE_PAUSED,
STATE_PLAYING,
)
from denonavr.exceptions import (
AvrCommandError,
AvrForbiddenError,
@@ -73,6 +81,23 @@ SERVICE_GET_COMMAND = "get_command"
SERVICE_SET_DYNAMIC_EQ = "set_dynamic_eq"
SERVICE_UPDATE_AUDYSSEY = "update_audyssey"
# HA Telnet events
TELNET_EVENTS = {
"HD",
"MS",
"MU",
"MV",
"NS",
"NSE",
"PS",
"SI",
"SS",
"TF",
"ZM",
"Z2",
"Z3",
}
_DenonDeviceT = TypeVar("_DenonDeviceT", bound="DenonDevice")
_R = TypeVar("_R")
_P = ParamSpec("_P")
@@ -254,7 +279,9 @@ class DenonDevice(MediaPlayerEntity):
async def _telnet_callback(self, zone, event, parameter) -> None:
"""Process a telnet command callback."""
# There are multiple checks implemented which reduce unnecessary updates of the ha state machine
if zone != self._receiver.zone:
if zone not in (self._receiver.zone, ALL_ZONES):
return
if event not in TELNET_EVENTS:
return
# Some updates trigger multiple events like one for artist and one for title for one change
# We skip every event except the last one
@@ -268,11 +295,11 @@ class DenonDevice(MediaPlayerEntity):
async def async_added_to_hass(self) -> None:
"""Register for telnet events."""
self._receiver.register_callback("ALL", self._telnet_callback)
self._receiver.register_callback(ALL_TELNET_EVENTS, self._telnet_callback)
async def async_will_remove_from_hass(self) -> None:
"""Clean up the entity."""
self._receiver.unregister_callback("ALL", self._telnet_callback)
self._receiver.unregister_callback(ALL_TELNET_EVENTS, self._telnet_callback)
@async_log_errors
async def async_update(self) -> None:
@@ -35,6 +35,7 @@ from .const import (
CONNECTED_PLC_DEVICES,
CONNECTED_WIFI_CLIENTS,
DOMAIN,
FIRMWARE_UPDATE_INTERVAL,
LONG_UPDATE_INTERVAL,
NEIGHBORING_WIFI_NETWORKS,
REGULAR_FIRMWARE,
@@ -146,7 +147,7 @@ async def async_setup_entry( # noqa: C901
_LOGGER,
name=REGULAR_FIRMWARE,
update_method=async_update_firmware_available,
update_interval=LONG_UPDATE_INTERVAL,
update_interval=FIRMWARE_UPDATE_INTERVAL,
)
if device.device and "wifi1" in device.device.features:
coordinators[CONNECTED_WIFI_CLIENTS] = DataUpdateCoordinator(
@@ -14,6 +14,7 @@ PRODUCT = "product"
SERIAL_NUMBER = "serial_number"
TITLE = "title"
FIRMWARE_UPDATE_INTERVAL = timedelta(hours=5)
LONG_UPDATE_INTERVAL = timedelta(minutes=5)
SHORT_UPDATE_INTERVAL = timedelta(seconds=15)
+1 -1
View File
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/doods",
"iot_class": "local_polling",
"loggers": ["pydoods"],
"requirements": ["pydoods==1.0.2", "Pillow==10.0.0"]
"requirements": ["pydoods==1.0.2", "Pillow==10.0.1"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/duotecno",
"iot_class": "local_push",
"requirements": ["pyDuotecno==2023.9.0"]
"requirements": ["pyDuotecno==2023.10.1"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.5.36"]
"requirements": ["env-canada==0.6.0"]
}
@@ -25,8 +25,11 @@ from aioesphomeapi import (
BluetoothProxyFeature,
DeviceInfo,
)
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
from aioesphomeapi.core import BluetoothGATTAPIError
from aioesphomeapi.core import (
APIConnectionError,
BluetoothGATTAPIError,
TimeoutAPIError,
)
from async_interrupt import interrupt
from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, NotifyCallback
@@ -389,8 +392,8 @@ class ESPHomeClient(BaseBleakClient):
return await self._disconnect()
async def _disconnect(self) -> bool:
self._async_disconnected_cleanup()
await self._client.bluetooth_device_disconnect(self._address_as_int)
self._async_ble_device_disconnected()
await self._wait_for_free_connection_slot(DISCONNECT_TIMEOUT)
return True
+8 -5
View File
@@ -327,20 +327,23 @@ class ESPHomeManager:
) -> int | None:
"""Start a voice assistant pipeline."""
if self.voice_assistant_udp_server is not None:
return None
_LOGGER.warning("Voice assistant UDP server was not stopped")
self.voice_assistant_udp_server.stop()
self.voice_assistant_udp_server.close()
self.voice_assistant_udp_server = None
hass = self.hass
voice_assistant_udp_server = VoiceAssistantUDPServer(
self.voice_assistant_udp_server = VoiceAssistantUDPServer(
hass,
self.entry_data,
self._handle_pipeline_event,
self._handle_pipeline_finished,
)
port = await voice_assistant_udp_server.start_server()
port = await self.voice_assistant_udp_server.start_server()
assert self.device_id is not None, "Device ID must be set"
hass.async_create_background_task(
voice_assistant_udp_server.run_pipeline(
self.voice_assistant_udp_server.run_pipeline(
device_id=self.device_id,
conversation_id=conversation_id or None,
flags=flags,
@@ -535,7 +538,7 @@ class ESPHomeManager:
on_connect=self.on_connect,
on_disconnect=self.on_disconnect,
zeroconf_instance=self.zeroconf_instance,
name=self.host,
name=entry.data.get(CONF_DEVICE_NAME, self.host),
on_connect_error=self.on_connect_error,
)
self.reconnect_logic = reconnect_logic
@@ -16,8 +16,8 @@
"loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [
"async-interrupt==1.1.1",
"aioesphomeapi==17.0.0",
"bluetooth-data-tools==1.12.0",
"aioesphomeapi==18.0.7",
"bluetooth-data-tools==1.13.0",
"esphome-dashboard-api==1.2.3"
],
"zeroconf": ["_esphomelib._tcp.local."]
@@ -55,6 +55,8 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_START: PipelineEventType.WAKE_WORD_START,
VoiceAssistantEventType.VOICE_ASSISTANT_WAKE_WORD_END: PipelineEventType.WAKE_WORD_END,
VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_START: PipelineEventType.STT_VAD_START,
VoiceAssistantEventType.VOICE_ASSISTANT_STT_VAD_END: PipelineEventType.STT_VAD_END,
}
)
@@ -161,7 +163,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
try:
event_type = _VOICE_ASSISTANT_EVENT_TYPES.from_hass(event.type)
except KeyError:
_LOGGER.warning("Received unknown pipeline event type: %s", event.type)
_LOGGER.debug("Received unknown pipeline event type: %s", event.type)
return
data_to_send = None
@@ -222,7 +224,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
audio_settings: VoiceAssistantAudioSettings | None = None,
) -> None:
"""Run the Voice Assistant pipeline."""
if audio_settings is None:
if audio_settings is None or audio_settings.volume_multiplier == 0:
audio_settings = VoiceAssistantAudioSettings()
tts_audio_output = (
@@ -260,6 +262,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
noise_suppression_level=audio_settings.noise_suppression_level,
auto_gain_dbfs=audio_settings.auto_gain,
volume_multiplier=audio_settings.volume_multiplier,
is_vad_enabled=bool(flags & VoiceAssistantCommandFlag.USE_VAD),
),
)
@@ -295,6 +298,10 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
if self.transport is None:
return
self.handle_event(
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_START, {}
)
_extension, audio_bytes = await tts.async_get_media_source_audio(
self.hass,
media_id,
@@ -320,4 +327,7 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
sample_offset += samples_in_chunk
finally:
self.handle_event(
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_STREAM_END, {}
)
self._tts_done.set()
@@ -24,5 +24,5 @@
"documentation": "https://www.home-assistant.io/integrations/eufylife_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["eufylife-ble-client==0.1.7"]
"requirements": ["eufylife-ble-client==0.1.8"]
}
@@ -12,5 +12,5 @@
"iot_class": "local_polling",
"loggers": ["pyfronius"],
"quality_scale": "platinum",
"requirements": ["PyFronius==0.7.1"]
"requirements": ["PyFronius==0.7.2"]
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20230928.0"]
"requirements": ["home-assistant-frontend==20231005.0"]
}
@@ -6,5 +6,5 @@
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/generic",
"iot_class": "local_push",
"requirements": ["ha-av==10.1.1", "Pillow==10.0.0"]
"requirements": ["ha-av==10.1.1", "Pillow==10.0.1"]
}
@@ -240,6 +240,7 @@ async def async_setup_entry(
SERVICE_CREATE_EVENT,
CREATE_EVENT_SCHEMA,
async_create_event,
required_features=CalendarEntityFeature.CREATE_EVENT,
)
@@ -118,9 +118,7 @@ class GoogleMapsScanner:
)
_LOGGER.debug("%s < %s", last_seen, self._prev_seen[dev_id])
continue
if last_seen == self._prev_seen.get(dev_id, last_seen) and hasattr(
self, "success_init"
):
if last_seen == self._prev_seen.get(dev_id):
_LOGGER.debug(
"Ignoring %s update because timestamp "
"is the same as the last timestamp %s",
@@ -3,7 +3,7 @@
"config": {
"step": {
"user": {
"description": "When specifying the origin and destination, you can supply one or more locations separated by the pipe character, in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
"description": "You can specify the origin and destination in the form of an address, latitude/longitude coordinates, or a Google place ID. When specifying the location using a Google place ID, the ID must be prefixed with `place_id:`.",
"data": {
"name": "[%key:common::config_flow::data::name%]",
"api_key": "[%key:common::config_flow::data::api_key%]",
@@ -9,6 +9,7 @@ from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
BOARD_NAME = "Home Assistant Green"
DOCUMENTATION_URL = "https://green.home-assistant.io/documentation/"
MANUFACTURER = "homeassistant"
MODEL = "green"
@@ -39,6 +40,6 @@ def async_info(hass: HomeAssistant) -> list[HardwareInfo]:
config_entries=config_entries,
dongle=None,
name=BOARD_NAME,
url=None,
url=DOCUMENTATION_URL,
)
]
@@ -885,7 +885,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow, ABC):
async def check_multi_pan_addon(hass: HomeAssistant) -> None:
"""Check the multi-PAN addon state, and start it if installed but not started.
"""Check the multiprotocol addon state, and start it if installed but not started.
Does nothing if Hass.io is not loaded.
Raises on error or if the add-on is installed but not started.
@@ -45,7 +45,7 @@ async def _async_usb_scan_done(hass: HomeAssistant, entry: ConfigEntry) -> None:
return
hw_discovery_data = {
"name": "SkyConnect Multi-PAN",
"name": "SkyConnect Multiprotocol",
"port": {
"path": get_zigbee_socket(),
},
@@ -76,7 +76,7 @@ class HomeAssistantSkyConnectOptionsFlow(silabs_multiprotocol_addon.OptionsFlowH
def _zha_name(self) -> str:
"""Return the ZHA name."""
return "SkyConnect Multi-PAN"
return "SkyConnect Multiprotocol"
def _hardware_name(self) -> str:
"""Return the name of the hardware."""
@@ -35,7 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hw_discovery_data = ZHA_HW_DISCOVERY_DATA
else:
hw_discovery_data = {
"name": "Yellow Multi-PAN",
"name": "Yellow Multiprotocol",
"port": {
"path": get_zigbee_socket(),
},
@@ -153,7 +153,7 @@ class HomeAssistantYellowOptionsFlow(silabs_multiprotocol_addon.OptionsFlowHandl
def _zha_name(self) -> str:
"""Return the ZHA name."""
return "Yellow Multi-PAN"
return "Yellow Multiprotocol"
def _hardware_name(self) -> str:
"""Return the name of the hardware."""
@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["pyhap"],
"requirements": [
"HAP-python==4.7.1",
"HAP-python==4.8.0",
"fnv-hash-fast==0.4.1",
"PyQRCode==1.2.1",
"base36==0.1.1"
@@ -4,9 +4,9 @@ from __future__ import annotations
import logging
from typing import Any
from bleak import BleakError
from bleak.exc import BleakError
from bluetooth_data_tools import human_readable_name
from idasen_ha import Desk
from idasen_ha import AuthFailedError, Desk
import voluptuous as vol
from homeassistant import config_entries
@@ -64,6 +64,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
desk = Desk(None)
try:
await desk.connect(discovery_info.device, monitor_height=False)
except AuthFailedError as err:
_LOGGER.exception("AuthFailedError", exc_info=err)
errors["base"] = "auth_failed"
except TimeoutError as err:
_LOGGER.exception("TimeoutError", exc_info=err)
errors["base"] = "cannot_connect"
@@ -11,5 +11,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/idasen_desk",
"iot_class": "local_push",
"requirements": ["idasen-ha==1.4"]
"requirements": ["idasen-ha==1.4.1"]
}
@@ -9,7 +9,8 @@
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"auth_failed": "Unable to authenticate with the desk. This is usually solved by using an ESPHome Bluetooth Proxy. Please check the integration documentation for alternative workarounds.",
"cannot_connect": "Cannot connect. Make sure that the desk is in Bluetooth pairing mode. If not already, you can also use an ESPHome Bluetooth Proxy, as it provides a better connection.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/image_upload",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["Pillow==10.0.0"]
"requirements": ["Pillow==10.0.1"]
}
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.12.0", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.13.0", "ld2410-ble==0.1.1"]
}
@@ -32,5 +32,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.12.0", "led-ble==1.0.1"]
"requirements": ["bluetooth-data-tools==1.13.0", "led-ble==1.0.1"]
}
@@ -12,5 +12,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"requirements": ["pylitterbot==2023.4.8"]
"requirements": ["pylitterbot==2023.4.9"]
}
+1 -1
View File
@@ -7,7 +7,7 @@
"dependencies": ["webhook"],
"documentation": "https://www.home-assistant.io/integrations/loqed",
"iot_class": "local_push",
"requirements": ["loqedAPI==2.1.7"],
"requirements": ["loqedAPI==2.1.8"],
"zeroconf": [
{
"type": "_http._tcp.local.",
@@ -9,7 +9,7 @@
},
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.18.2"],
"requirements": ["pylutron-caseta==0.18.3"],
"zeroconf": [
{
"type": "_lutron._tcp.local.",
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/matrix",
"iot_class": "cloud_push",
"loggers": ["matrix_client"],
"requirements": ["matrix-nio==0.21.2", "Pillow==10.0.0"]
"requirements": ["matrix-nio==0.21.2", "Pillow==10.0.1"]
}
+2 -2
View File
@@ -250,9 +250,9 @@ class MatterClimate(MatterEntity, ClimateEntity):
self._attr_min_temp = DEFAULT_MIN_TEMP
# update max_temp
if self._attr_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL):
attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit
else:
attribute = clusters.Thermostat.Attributes.AbsMaxCoolSetpointLimit
else:
attribute = clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit
if (value := self._get_temperature_in_degrees(attribute)) is not None:
self._attr_max_temp = value
else:
+21 -242
View File
@@ -1,213 +1,26 @@
"""The Mazda Connected Services integration."""
from __future__ import annotations
import asyncio
from datetime import timedelta
import logging
from typing import TYPE_CHECKING
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import issue_registry as ir
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaTokenExpiredException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
UpdateFailed,
)
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DATA_VEHICLES, DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
Platform.DEVICE_TRACKER,
Platform.LOCK,
Platform.SENSOR,
Platform.SWITCH,
]
DOMAIN = "mazda"
async def with_timeout(task, timeout_seconds=30):
"""Run an async task with a timeout."""
async with asyncio.timeout(timeout_seconds):
return await task
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, _: ConfigEntry) -> bool:
"""Set up Mazda Connected Services from a config entry."""
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
region = entry.data[CONF_REGION]
websession = aiohttp_client.async_get_clientsession(hass)
mazda_client = MazdaAPI(
email, password, region, websession=websession, use_cached_vehicle_list=True
)
try:
await mazda_client.validate_credentials()
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed from ex
except (
MazdaException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
) as ex:
_LOGGER.error("Error occurred during Mazda login request: %s", ex)
raise ConfigEntryNotReady from ex
async def async_handle_service_call(service_call: ServiceCall) -> None:
"""Handle a service call."""
# Get device entry from device registry
dev_reg = dr.async_get(hass)
device_id = service_call.data["device_id"]
device_entry = dev_reg.async_get(device_id)
if TYPE_CHECKING:
# For mypy: it has already been checked in validate_mazda_device_id
assert device_entry
# Get vehicle VIN from device identifiers
mazda_identifiers = (
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
)
vin_identifier = next(mazda_identifiers)
vin = vin_identifier[1]
# Get vehicle ID and API client from hass.data
vehicle_id = 0
api_client = None
for entry_data in hass.data[DOMAIN].values():
for vehicle in entry_data[DATA_VEHICLES]:
if vehicle["vin"] == vin:
vehicle_id = vehicle["id"]
api_client = entry_data[DATA_CLIENT]
break
if vehicle_id == 0 or api_client is None:
raise HomeAssistantError("Vehicle ID not found")
api_method = getattr(api_client, service_call.service)
try:
latitude = service_call.data["latitude"]
longitude = service_call.data["longitude"]
poi_name = service_call.data["poi_name"]
await api_method(vehicle_id, latitude, longitude, poi_name)
except Exception as ex:
raise HomeAssistantError(ex) from ex
def validate_mazda_device_id(device_id):
"""Check that a device ID exists in the registry and has at least one 'mazda' identifier."""
dev_reg = dr.async_get(hass)
if (device_entry := dev_reg.async_get(device_id)) is None:
raise vol.Invalid("Invalid device ID")
mazda_identifiers = [
identifier
for identifier in device_entry.identifiers
if identifier[0] == DOMAIN
]
if not mazda_identifiers:
raise vol.Invalid("Device ID is not a Mazda vehicle")
return device_id
service_schema_send_poi = vol.Schema(
{
vol.Required("device_id"): vol.All(cv.string, validate_mazda_device_id),
vol.Required("latitude"): cv.latitude,
vol.Required("longitude"): cv.longitude,
vol.Required("poi_name"): cv.string,
}
)
async def async_update_data():
"""Fetch data from Mazda API."""
try:
vehicles = await with_timeout(mazda_client.get_vehicles())
# The Mazda API can throw an error when multiple simultaneous requests are
# made for the same account, so we can only make one request at a time here
for vehicle in vehicles:
vehicle["status"] = await with_timeout(
mazda_client.get_vehicle_status(vehicle["id"])
)
# If vehicle is electric, get additional EV-specific status info
if vehicle["isElectric"]:
vehicle["evStatus"] = await with_timeout(
mazda_client.get_ev_vehicle_status(vehicle["id"])
)
vehicle["hvacSetting"] = await with_timeout(
mazda_client.get_hvac_setting(vehicle["id"])
)
hass.data[DOMAIN][entry.entry_id][DATA_VEHICLES] = vehicles
return vehicles
except MazdaAuthenticationException as ex:
raise ConfigEntryAuthFailed("Not authenticated with Mazda API") from ex
except Exception as ex:
_LOGGER.exception(
"Unknown error occurred during Mazda update request: %s", ex
)
raise UpdateFailed(ex) from ex
coordinator = DataUpdateCoordinator(
ir.async_create_issue(
hass,
_LOGGER,
name=DOMAIN,
update_method=async_update_data,
update_interval=timedelta(seconds=180),
)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_CLIENT: mazda_client,
DATA_COORDINATOR: coordinator,
DATA_REGION: region,
DATA_VEHICLES: [],
}
# Fetch initial data so we have data when entities subscribe
await coordinator.async_config_entry_first_refresh()
# Setup components
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register services
hass.services.async_register(
DOMAIN,
"send_poi",
async_handle_service_call,
schema=service_schema_send_poi,
DOMAIN,
is_fixable=False,
severity=ir.IssueSeverity.ERROR,
translation_key="integration_removed",
translation_placeholders={
"dmca": "https://github.com/github/dmca/blob/master/2023/10/2023-10-10-mazda.md",
"entries": "/config/integrations/integration/mazda",
},
)
return True
@@ -215,45 +28,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if all(
config_entry.state is ConfigEntryState.NOT_LOADED
for config_entry in hass.config_entries.async_entries(DOMAIN)
if config_entry.entry_id != entry.entry_id
):
ir.async_delete_issue(hass, DOMAIN, DOMAIN)
# Only remove services if it is the last config entry
if len(hass.data[DOMAIN]) == 1:
hass.services.async_remove(DOMAIN, "send_poi")
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
class MazdaEntity(CoordinatorEntity):
"""Defines a base Mazda entity."""
_attr_has_entity_name = True
def __init__(self, client, coordinator, index):
"""Initialize the Mazda entity."""
super().__init__(coordinator)
self.client = client
self.index = index
self.vin = self.data["vin"]
self.vehicle_id = self.data["id"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.vin)},
manufacturer="Mazda",
model=f"{self.data['modelYear']} {self.data['carlineName']}",
name=self.vehicle_name,
)
@property
def data(self):
"""Shortcut to access coordinator data for the entity."""
return self.coordinator.data[self.index]
@property
def vehicle_name(self):
"""Return the vehicle name, to be used as a prefix for names of other entities."""
if "nickname" in self.data and len(self.data["nickname"]) > 0:
return self.data["nickname"]
return f"{self.data['modelYear']} {self.data['carlineName']}"
return True
@@ -1,131 +0,0 @@
"""Platform for Mazda binary sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaBinarySensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this binary sensor, given the coordinator data
value_fn: Callable[[dict[str, Any]], bool]
@dataclass
class MazdaBinarySensorEntityDescription(
BinarySensorEntityDescription, MazdaBinarySensorRequiredKeysMixin
):
"""Describes a Mazda binary sensor entity."""
# Function to determine whether the vehicle supports this binary sensor, given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
def _plugged_in_supported(data):
"""Determine if 'plugged in' binary sensor is supported."""
return (
data["isElectric"] and data["evStatus"]["chargeInfo"]["pluggedIn"] is not None
)
BINARY_SENSOR_ENTITIES = [
MazdaBinarySensorEntityDescription(
key="driver_door",
translation_key="driver_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["driverDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="passenger_door",
translation_key="passenger_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["passengerDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_left_door",
translation_key="rear_left_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearLeftDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="rear_right_door",
translation_key="rear_right_door",
icon="mdi:car-door",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["rearRightDoorOpen"],
),
MazdaBinarySensorEntityDescription(
key="trunk",
translation_key="trunk",
icon="mdi:car-back",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["trunkOpen"],
),
MazdaBinarySensorEntityDescription(
key="hood",
translation_key="hood",
icon="mdi:car",
device_class=BinarySensorDeviceClass.DOOR,
value_fn=lambda data: data["status"]["doors"]["hoodOpen"],
),
MazdaBinarySensorEntityDescription(
key="ev_plugged_in",
translation_key="ev_plugged_in",
device_class=BinarySensorDeviceClass.PLUG,
is_supported=_plugged_in_supported,
value_fn=lambda data: data["evStatus"]["chargeInfo"]["pluggedIn"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaBinarySensorEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BINARY_SENSOR_ENTITIES
if description.is_supported(data)
)
class MazdaBinarySensorEntity(MazdaEntity, BinarySensorEntity):
"""Representation of a Mazda vehicle binary sensor."""
entity_description: MazdaBinarySensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda binary sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def is_on(self):
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.data)
-150
View File
@@ -1,150 +0,0 @@
"""Platform for Mazda button integration."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from pymazda import (
Client as MazdaAPIClient,
MazdaAccountLockedException,
MazdaAPIEncryptionException,
MazdaAuthenticationException,
MazdaException,
MazdaLoginFailedException,
MazdaTokenExpiredException,
)
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def handle_button_press(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a press for a Mazda button entity."""
api_method = getattr(client, key)
try:
await api_method(vehicle_id)
except (
MazdaException,
MazdaAuthenticationException,
MazdaAccountLockedException,
MazdaTokenExpiredException,
MazdaAPIEncryptionException,
MazdaLoginFailedException,
) as ex:
raise HomeAssistantError(ex) from ex
async def handle_refresh_vehicle_status(
client: MazdaAPIClient,
key: str,
vehicle_id: int,
coordinator: DataUpdateCoordinator,
) -> None:
"""Handle a request to refresh the vehicle status."""
await handle_button_press(client, key, vehicle_id, coordinator)
await coordinator.async_request_refresh()
@dataclass
class MazdaButtonEntityDescription(ButtonEntityDescription):
"""Describes a Mazda button entity."""
# Function to determine whether the vehicle supports this button,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
async_press: Callable[
[MazdaAPIClient, str, int, DataUpdateCoordinator], Awaitable
] = handle_button_press
BUTTON_ENTITIES = [
MazdaButtonEntityDescription(
key="start_engine",
translation_key="start_engine",
icon="mdi:engine",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="stop_engine",
translation_key="stop_engine",
icon="mdi:engine-off",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_on_hazard_lights",
translation_key="turn_on_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="turn_off_hazard_lights",
translation_key="turn_off_hazard_lights",
icon="mdi:hazard-lights",
is_supported=lambda data: not data["isElectric"],
),
MazdaButtonEntityDescription(
key="refresh_vehicle_status",
translation_key="refresh_vehicle_status",
icon="mdi:refresh",
async_press=handle_refresh_vehicle_status,
is_supported=lambda data: data["isElectric"],
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the button platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaButtonEntity(client, coordinator, index, description)
for index, data in enumerate(coordinator.data)
for description in BUTTON_ENTITIES
if description.is_supported(data)
)
class MazdaButtonEntity(MazdaEntity, ButtonEntity):
"""Representation of a Mazda button."""
entity_description: MazdaButtonEntityDescription
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
description: MazdaButtonEntityDescription,
) -> None:
"""Initialize Mazda button."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.async_press(
self.client, self.entity_description.key, self.vehicle_id, self.coordinator
)
-187
View File
@@ -1,187 +0,0 @@
"""Platform for Mazda climate integration."""
from __future__ import annotations
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_HALVES,
PRECISION_WHOLE,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util.unit_conversion import TemperatureConverter
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DATA_REGION, DOMAIN
PRESET_DEFROSTER_OFF = "Defroster Off"
PRESET_DEFROSTER_FRONT = "Front Defroster"
PRESET_DEFROSTER_REAR = "Rear Defroster"
PRESET_DEFROSTER_FRONT_AND_REAR = "Front and Rear Defroster"
def _front_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_FRONT,
]
def _rear_defroster_enabled(preset_mode: str | None) -> bool:
return preset_mode in [
PRESET_DEFROSTER_FRONT_AND_REAR,
PRESET_DEFROSTER_REAR,
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the climate platform."""
entry_data = hass.data[DOMAIN][config_entry.entry_id]
client = entry_data[DATA_CLIENT]
coordinator = entry_data[DATA_COORDINATOR]
region = entry_data[DATA_REGION]
async_add_entities(
MazdaClimateEntity(client, coordinator, index, region)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaClimateEntity(MazdaEntity, ClimateEntity):
"""Class for a Mazda climate entity."""
_attr_translation_key = "climate"
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_hvac_modes = [HVACMode.HEAT_COOL, HVACMode.OFF]
_attr_preset_modes = [
PRESET_DEFROSTER_OFF,
PRESET_DEFROSTER_FRONT,
PRESET_DEFROSTER_REAR,
PRESET_DEFROSTER_FRONT_AND_REAR,
]
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
region: str,
) -> None:
"""Initialize Mazda climate entity."""
super().__init__(client, coordinator, index)
self.region = region
self._attr_unique_id = self.vin
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_precision = PRECISION_WHOLE
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
self._attr_min_temp = 61.0
self._attr_max_temp = 83.0
else:
self._attr_precision = PRECISION_HALVES
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
if region == "MJO":
self._attr_min_temp = 18.5
self._attr_max_temp = 31.5
else:
self._attr_min_temp = 15.5
self._attr_max_temp = 28.5
self._update_state_attributes()
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator data updates."""
self._update_state_attributes()
super()._handle_coordinator_update()
def _update_state_attributes(self) -> None:
# Update the HVAC mode
hvac_on = self.client.get_assumed_hvac_mode(self.vehicle_id)
self._attr_hvac_mode = HVACMode.HEAT_COOL if hvac_on else HVACMode.OFF
# Update the target temperature
hvac_setting = self.client.get_assumed_hvac_setting(self.vehicle_id)
self._attr_target_temperature = hvac_setting.get("temperature")
# Update the current temperature
current_temperature_celsius = self.data["evStatus"]["hvacInfo"][
"interiorTemperatureCelsius"
]
if self.data["hvacSetting"]["temperatureUnit"] == "F":
self._attr_current_temperature = TemperatureConverter.convert(
current_temperature_celsius,
UnitOfTemperature.CELSIUS,
UnitOfTemperature.FAHRENHEIT,
)
else:
self._attr_current_temperature = current_temperature_celsius
# Update the preset mode based on the state of the front and rear defrosters
front_defroster = hvac_setting.get("frontDefroster")
rear_defroster = hvac_setting.get("rearDefroster")
if front_defroster and rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT_AND_REAR
elif front_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_FRONT
elif rear_defroster:
self._attr_preset_mode = PRESET_DEFROSTER_REAR
else:
self._attr_preset_mode = PRESET_DEFROSTER_OFF
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set a new HVAC mode."""
if hvac_mode == HVACMode.HEAT_COOL:
await self.client.turn_on_hvac(self.vehicle_id)
elif hvac_mode == HVACMode.OFF:
await self.client.turn_off_hvac(self.vehicle_id)
self._handle_coordinator_update()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set a new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
precision = self.precision
rounded_temperature = round(temperature / precision) * precision
await self.client.set_hvac_setting(
self.vehicle_id,
rounded_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(self._attr_preset_mode),
_rear_defroster_enabled(self._attr_preset_mode),
)
self._handle_coordinator_update()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Turn on/off the front/rear defrosters according to the chosen preset mode."""
await self.client.set_hvac_setting(
self.vehicle_id,
self._attr_target_temperature,
self.data["hvacSetting"]["temperatureUnit"],
_front_defroster_enabled(preset_mode),
_rear_defroster_enabled(preset_mode),
)
self._handle_coordinator_update()
+4 -103
View File
@@ -1,110 +1,11 @@
"""Config flow for Mazda Connected Services integration."""
from collections.abc import Mapping
import logging
from typing import Any
"""The Mazda Connected Services integration."""
import aiohttp
from pymazda import (
Client as MazdaAPI,
MazdaAccountLockedException,
MazdaAuthenticationException,
)
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import aiohttp_client
from .const import DOMAIN, MAZDA_REGIONS
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION): vol.In(MAZDA_REGIONS),
}
)
from . import DOMAIN
class MazdaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
class MazdaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Mazda Connected Services."""
VERSION = 1
def __init__(self):
"""Start the mazda config flow."""
self._reauth_entry = None
self._email = None
self._region = None
async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
self._email = user_input[CONF_EMAIL]
self._region = user_input[CONF_REGION]
unique_id = user_input[CONF_EMAIL].lower()
await self.async_set_unique_id(unique_id)
if not self._reauth_entry:
self._abort_if_unique_id_configured()
websession = aiohttp_client.async_get_clientsession(self.hass)
mazda_client = MazdaAPI(
user_input[CONF_EMAIL],
user_input[CONF_PASSWORD],
user_input[CONF_REGION],
websession,
)
try:
await mazda_client.validate_credentials()
except MazdaAuthenticationException:
errors["base"] = "invalid_auth"
except MazdaAccountLockedException:
errors["base"] = "account_locked"
except aiohttp.ClientError:
errors["base"] = "cannot_connect"
except Exception as ex: # pylint: disable=broad-except
errors["base"] = "unknown"
_LOGGER.exception(
"Unknown error occurred during Mazda login request: %s", ex
)
else:
if not self._reauth_entry:
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
self.hass.config_entries.async_update_entry(
self._reauth_entry, data=user_input, unique_id=unique_id
)
# Reload the config entry otherwise devices will remain unavailable
self.hass.async_create_task(
self.hass.config_entries.async_reload(self._reauth_entry.entry_id)
)
return self.async_abort(reason="reauth_successful")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_EMAIL, default=self._email): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_REGION, default=self._region): vol.In(
MAZDA_REGIONS
),
}
),
errors=errors,
)
async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult:
"""Perform reauth if the user credentials have changed."""
self._reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
)
self._email = entry_data[CONF_EMAIL]
self._region = entry_data[CONF_REGION]
return await self.async_step_user()
-10
View File
@@ -1,10 +0,0 @@
"""Constants for the Mazda Connected Services integration."""
DOMAIN = "mazda"
DATA_CLIENT = "mazda_client"
DATA_COORDINATOR = "coordinator"
DATA_REGION = "region"
DATA_VEHICLES = "vehicles"
MAZDA_REGIONS = {"MNAO": "North America", "MME": "Europe", "MJO": "Japan"}
@@ -1,54 +0,0 @@
"""Platform for Mazda device tracker integration."""
from homeassistant.components.device_tracker import SourceType, TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the device tracker platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities = []
for index, _ in enumerate(coordinator.data):
entities.append(MazdaDeviceTracker(client, coordinator, index))
async_add_entities(entities)
class MazdaDeviceTracker(MazdaEntity, TrackerEntity):
"""Class for the device tracker."""
_attr_translation_key = "device_tracker"
_attr_icon = "mdi:car"
_attr_force_update = False
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda device tracker."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def source_type(self) -> SourceType:
"""Return the source type, eg gps or router, of the device."""
return SourceType.GPS
@property
def latitude(self):
"""Return latitude value of the device."""
return self.data["status"]["latitude"]
@property
def longitude(self):
"""Return longitude value of the device."""
return self.data["status"]["longitude"]
@@ -1,57 +0,0 @@
"""Diagnostics support for the Mazda integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics.util import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceEntry
from .const import DATA_COORDINATOR, DOMAIN
TO_REDACT_INFO = [CONF_EMAIL, CONF_PASSWORD]
TO_REDACT_DATA = ["vin", "id", "latitude", "longitude"]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": [
async_redact_data(vehicle, TO_REDACT_DATA) for vehicle in coordinator.data
],
}
return diagnostics_data
async def async_get_device_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry, device: DeviceEntry
) -> dict[str, Any]:
"""Return diagnostics for a device."""
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
vin = next(iter(device.identifiers))[1]
target_vehicle = None
for vehicle in coordinator.data:
if vehicle["vin"] == vin:
target_vehicle = vehicle
break
if target_vehicle is None:
raise HomeAssistantError("Vehicle not found")
diagnostics_data = {
"info": async_redact_data(config_entry.data, TO_REDACT_INFO),
"data": async_redact_data(target_vehicle, TO_REDACT_DATA),
}
return diagnostics_data
-58
View File
@@ -1,58 +0,0 @@
"""Platform for Mazda lock integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.lock import LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the lock platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities = []
for index, _ in enumerate(coordinator.data):
entities.append(MazdaLock(client, coordinator, index))
async_add_entities(entities)
class MazdaLock(MazdaEntity, LockEntity):
"""Class for the lock."""
_attr_translation_key = "lock"
def __init__(self, client, coordinator, index) -> None:
"""Initialize Mazda lock."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_locked(self) -> bool | None:
"""Return true if lock is locked."""
return self.client.get_assumed_lock_state(self.vehicle_id)
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the vehicle doors."""
await self.client.lock_doors(self.vehicle_id)
self.async_write_ha_state()
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the vehicle doors."""
await self.client.unlock_doors(self.vehicle_id)
self.async_write_ha_state()
+3 -5
View File
@@ -1,11 +1,9 @@
{
"domain": "mazda",
"name": "Mazda Connected Services",
"codeowners": ["@bdr99"],
"config_flow": true,
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/mazda",
"integration_type": "system",
"iot_class": "cloud_polling",
"loggers": ["pymazda"],
"quality_scale": "platinum",
"requirements": ["pymazda==0.3.11"]
"requirements": []
}
-263
View File
@@ -1,263 +0,0 @@
"""Platform for Mazda sensor integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, UnitOfLength, UnitOfPressure
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
@dataclass
class MazdaSensorRequiredKeysMixin:
"""Mixin for required keys."""
# Function to determine the value for this sensor, given the coordinator data
# and the configured unit system
value: Callable[[dict[str, Any]], StateType]
@dataclass
class MazdaSensorEntityDescription(
SensorEntityDescription, MazdaSensorRequiredKeysMixin
):
"""Describes a Mazda sensor entity."""
# Function to determine whether the vehicle supports this sensor,
# given the coordinator data
is_supported: Callable[[dict[str, Any]], bool] = lambda data: True
def _fuel_remaining_percentage_supported(data):
"""Determine if fuel remaining percentage is supported."""
return (not data["isElectric"]) and (
data["status"]["fuelRemainingPercent"] is not None
)
def _fuel_distance_remaining_supported(data):
"""Determine if fuel distance remaining is supported."""
return (not data["isElectric"]) and (
data["status"]["fuelDistanceRemainingKm"] is not None
)
def _front_left_tire_pressure_supported(data):
"""Determine if front left tire pressure is supported."""
return data["status"]["tirePressure"]["frontLeftTirePressurePsi"] is not None
def _front_right_tire_pressure_supported(data):
"""Determine if front right tire pressure is supported."""
return data["status"]["tirePressure"]["frontRightTirePressurePsi"] is not None
def _rear_left_tire_pressure_supported(data):
"""Determine if rear left tire pressure is supported."""
return data["status"]["tirePressure"]["rearLeftTirePressurePsi"] is not None
def _rear_right_tire_pressure_supported(data):
"""Determine if rear right tire pressure is supported."""
return data["status"]["tirePressure"]["rearRightTirePressurePsi"] is not None
def _ev_charge_level_supported(data):
"""Determine if charge level is supported."""
return (
data["isElectric"]
and data["evStatus"]["chargeInfo"]["batteryLevelPercentage"] is not None
)
def _ev_remaining_range_supported(data):
"""Determine if remaining range is supported."""
return (
data["isElectric"]
and data["evStatus"]["chargeInfo"]["drivingRangeKm"] is not None
)
def _fuel_distance_remaining_value(data):
"""Get the fuel distance remaining value."""
return round(data["status"]["fuelDistanceRemainingKm"])
def _odometer_value(data):
"""Get the odometer value."""
# In order to match the behavior of the Mazda mobile app, we always round down
return int(data["status"]["odometerKm"])
def _front_left_tire_pressure_value(data):
"""Get the front left tire pressure value."""
return round(data["status"]["tirePressure"]["frontLeftTirePressurePsi"])
def _front_right_tire_pressure_value(data):
"""Get the front right tire pressure value."""
return round(data["status"]["tirePressure"]["frontRightTirePressurePsi"])
def _rear_left_tire_pressure_value(data):
"""Get the rear left tire pressure value."""
return round(data["status"]["tirePressure"]["rearLeftTirePressurePsi"])
def _rear_right_tire_pressure_value(data):
"""Get the rear right tire pressure value."""
return round(data["status"]["tirePressure"]["rearRightTirePressurePsi"])
def _ev_charge_level_value(data):
"""Get the charge level value."""
return round(data["evStatus"]["chargeInfo"]["batteryLevelPercentage"])
def _ev_remaining_range_value(data):
"""Get the remaining range value."""
return round(data["evStatus"]["chargeInfo"]["drivingRangeKm"])
SENSOR_ENTITIES = [
MazdaSensorEntityDescription(
key="fuel_remaining_percentage",
translation_key="fuel_remaining_percentage",
icon="mdi:gas-station",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_remaining_percentage_supported,
value=lambda data: data["status"]["fuelRemainingPercent"],
),
MazdaSensorEntityDescription(
key="fuel_distance_remaining",
translation_key="fuel_distance_remaining",
icon="mdi:gas-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_fuel_distance_remaining_supported,
value=_fuel_distance_remaining_value,
),
MazdaSensorEntityDescription(
key="odometer",
translation_key="odometer",
icon="mdi:speedometer",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.TOTAL_INCREASING,
is_supported=lambda data: data["status"]["odometerKm"] is not None,
value=_odometer_value,
),
MazdaSensorEntityDescription(
key="front_left_tire_pressure",
translation_key="front_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_left_tire_pressure_supported,
value=_front_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="front_right_tire_pressure",
translation_key="front_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_front_right_tire_pressure_supported,
value=_front_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_left_tire_pressure",
translation_key="rear_left_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_left_tire_pressure_supported,
value=_rear_left_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="rear_right_tire_pressure",
translation_key="rear_right_tire_pressure",
icon="mdi:car-tire-alert",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=UnitOfPressure.PSI,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_rear_right_tire_pressure_supported,
value=_rear_right_tire_pressure_value,
),
MazdaSensorEntityDescription(
key="ev_charge_level",
translation_key="ev_charge_level",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_charge_level_supported,
value=_ev_charge_level_value,
),
MazdaSensorEntityDescription(
key="ev_remaining_range",
translation_key="ev_remaining_range",
icon="mdi:ev-station",
device_class=SensorDeviceClass.DISTANCE,
native_unit_of_measurement=UnitOfLength.KILOMETERS,
state_class=SensorStateClass.MEASUREMENT,
is_supported=_ev_remaining_range_supported,
value=_ev_remaining_range_value,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
entities: list[SensorEntity] = []
for index, data in enumerate(coordinator.data):
for description in SENSOR_ENTITIES:
if description.is_supported(data):
entities.append(
MazdaSensorEntity(client, coordinator, index, description)
)
async_add_entities(entities)
class MazdaSensorEntity(MazdaEntity, SensorEntity):
"""Representation of a Mazda vehicle sensor."""
entity_description: MazdaSensorEntityDescription
def __init__(self, client, coordinator, index, description):
"""Initialize Mazda sensor."""
super().__init__(client, coordinator, index)
self.entity_description = description
self._attr_unique_id = f"{self.vin}_{description.key}"
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self.entity_description.value(self.data)
@@ -1,30 +0,0 @@
send_poi:
fields:
device_id:
required: true
selector:
device:
integration: mazda
latitude:
example: 12.34567
required: true
selector:
number:
min: -90
max: 90
unit_of_measurement: °
mode: box
longitude:
example: -34.56789
required: true
selector:
number:
min: -180
max: 180
unit_of_measurement: °
mode: box
poi_name:
example: Work
required: true
selector:
text:
+4 -135
View File
@@ -1,139 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"account_locked": "Account locked. Please try again later.",
"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%]"
},
"step": {
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]",
"region": "Region"
},
"description": "Please enter the email address and password you use to log into the MyMazda mobile app."
}
}
},
"entity": {
"binary_sensor": {
"driver_door": {
"name": "Driver door"
},
"passenger_door": {
"name": "Passenger door"
},
"rear_left_door": {
"name": "Rear left door"
},
"rear_right_door": {
"name": "Rear right door"
},
"trunk": {
"name": "Trunk"
},
"hood": {
"name": "Hood"
},
"ev_plugged_in": {
"name": "Plugged in"
}
},
"button": {
"start_engine": {
"name": "Start engine"
},
"stop_engine": {
"name": "Stop engine"
},
"turn_on_hazard_lights": {
"name": "Turn on hazard lights"
},
"turn_off_hazard_lights": {
"name": "Turn off hazard lights"
},
"refresh_vehicle_status": {
"name": "Refresh status"
}
},
"climate": {
"climate": {
"name": "[%key:component::climate::title%]"
}
},
"device_tracker": {
"device_tracker": {
"name": "[%key:component::device_tracker::title%]"
}
},
"lock": {
"lock": {
"name": "[%key:component::lock::title%]"
}
},
"sensor": {
"fuel_remaining_percentage": {
"name": "Fuel remaining percentage"
},
"fuel_distance_remaining": {
"name": "Fuel distance remaining"
},
"odometer": {
"name": "Odometer"
},
"front_left_tire_pressure": {
"name": "Front left tire pressure"
},
"front_right_tire_pressure": {
"name": "Front right tire pressure"
},
"rear_left_tire_pressure": {
"name": "Rear left tire pressure"
},
"rear_right_tire_pressure": {
"name": "Rear right tire pressure"
},
"ev_charge_level": {
"name": "Charge level"
},
"ev_remaining_range": {
"name": "Remaining range"
}
},
"switch": {
"charging": {
"name": "Charging"
}
}
},
"services": {
"send_poi": {
"name": "Send POI",
"description": "Sends a GPS location to the vehicle's navigation system as a POI (Point of Interest). Requires a navigation SD card installed in the vehicle.",
"fields": {
"device_id": {
"name": "Vehicle",
"description": "The vehicle to send the GPS location to."
},
"latitude": {
"name": "[%key:common::config_flow::data::latitude%]",
"description": "The latitude of the location to send."
},
"longitude": {
"name": "[%key:common::config_flow::data::longitude%]",
"description": "The longitude of the location to send."
},
"poi_name": {
"name": "POI name",
"description": "A friendly name for the location."
}
}
"issues": {
"integration_removed": {
"title": "The Mazda integration has been removed",
"description": "The Mazda integration has been removed from Home Assistant.\n\nThe library that Home Assistant uses to connect with their services, [has been taken offline by Mazda]({dmca}).\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing Mazda integration entries]({entries})."
}
}
}
-72
View File
@@ -1,72 +0,0 @@
"""Platform for Mazda switch integration."""
from typing import Any
from pymazda import Client as MazdaAPIClient
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import MazdaEntity
from .const import DATA_CLIENT, DATA_COORDINATOR, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the switch platform."""
client = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT]
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
async_add_entities(
MazdaChargingSwitch(client, coordinator, index)
for index, data in enumerate(coordinator.data)
if data["isElectric"]
)
class MazdaChargingSwitch(MazdaEntity, SwitchEntity):
"""Class for the charging switch."""
_attr_translation_key = "charging"
_attr_icon = "mdi:ev-station"
def __init__(
self,
client: MazdaAPIClient,
coordinator: DataUpdateCoordinator,
index: int,
) -> None:
"""Initialize Mazda charging switch."""
super().__init__(client, coordinator, index)
self._attr_unique_id = self.vin
@property
def is_on(self):
"""Return true if the vehicle is charging."""
return self.data["evStatus"]["chargeInfo"]["charging"]
async def refresh_status_and_write_state(self):
"""Request a status update, retrieve it through the coordinator, and write the state."""
await self.client.refresh_vehicle_status(self.vehicle_id)
await self.coordinator.async_request_refresh()
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Start charging the vehicle."""
await self.client.start_charging(self.vehicle_id)
await self.refresh_status_and_write_state()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Stop charging the vehicle."""
await self.client.stop_charging(self.vehicle_id)
await self.refresh_status_and_write_state()
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mill",
"iot_class": "local_polling",
"loggers": ["mill", "mill_local"],
"requirements": ["millheater==0.11.5", "mill-local==0.3.0"]
"requirements": ["millheater==0.11.6", "mill-local==0.3.0"]
}
@@ -84,7 +84,7 @@ DEFAULT_STRUCT_FORMAT = {
DataType.INT64: ENTRY("q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.UINT64: ENTRY("Q", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.FLOAT64: ENTRY("d", 4, PARM_IS_LEGAL(False, False, True, True, True)),
DataType.STRING: ENTRY("s", 1, PARM_IS_LEGAL(True, False, False, False, False)),
DataType.STRING: ENTRY("s", -1, PARM_IS_LEGAL(True, False, False, False, False)),
DataType.CUSTOM: ENTRY("?", 0, PARM_IS_LEGAL(True, True, False, False, False)),
}
@@ -143,7 +143,8 @@ def struct_validator(config: dict[str, Any]) -> dict[str, Any]:
f"{name}: Size of structure is {size} bytes but `{CONF_COUNT}: {count}` is {bytecount} bytes"
)
else:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
if data_type != DataType.STRING:
config[CONF_COUNT] = DEFAULT_STRUCT_FORMAT[data_type].register_count
if slave_count:
structure = (
f">{slave_count + 1}{DEFAULT_STRUCT_FORMAT[data_type].struct_id}"
@@ -180,7 +180,7 @@ class MqttBinarySensor(MqttEntity, BinarySensorEntity, RestoreEntity):
@callback
@log_messages(self.hass, self.entity_id)
@write_state_on_attr_change(self, {"_attr_is_on"})
@write_state_on_attr_change(self, {"_attr_is_on", "_expired"})
def state_message_received(msg: ReceiveMessage) -> None:
"""Handle a new received MQTT state message."""
# auto-expire enabled?
+7 -1
View File
@@ -176,7 +176,13 @@ async def async_subscribe(
raise HomeAssistantError(
f"Cannot subscribe to topic '{topic}', MQTT is not enabled"
)
mqtt_data = get_mqtt_data(hass)
try:
mqtt_data = get_mqtt_data(hass)
except KeyError as ex:
raise HomeAssistantError(
f"Cannot subscribe to topic '{topic}', "
"make sure MQTT is set up correctly"
) from ex
async_remove = await mqtt_data.client.async_subscribe(
topic,
catch_log_exception(
@@ -52,7 +52,7 @@ DEFAULT_SOURCE_TYPE = SourceType.GPS
def valid_config(config: ConfigType) -> ConfigType:
"""Check if there is a state topic or json_attributes_topic."""
if CONF_STATE_TOPIC not in config and CONF_JSON_ATTRS_TOPIC not in config:
raise vol.MultipleInvalid(
raise vol.Invalid(
f"Invalid device tracker config, missing {CONF_STATE_TOPIC} or {CONF_JSON_ATTRS_TOPIC}, got: {config}"
)
return config
+3 -1
View File
@@ -277,7 +277,9 @@ class MqttSensor(MqttEntity, RestoreSensor):
)
@callback
@write_state_on_attr_change(self, {"_attr_native_value", "_attr_last_reset"})
@write_state_on_attr_change(
self, {"_attr_native_value", "_attr_last_reset", "_expired"}
)
@log_messages(self.hass, self.entity_id)
def message_received(msg: ReceiveMessage) -> None:
"""Handle new MQTT messages."""
+1 -1
View File
@@ -14,5 +14,5 @@
},
"iot_class": "cloud_polling",
"loggers": ["pkce", "pymyq"],
"requirements": ["python-myq==3.1.9"]
"requirements": ["python-myq==3.1.13"]
}
+3 -1
View File
@@ -8,7 +8,7 @@ from typing import Any
from mysensors import BaseAsyncGateway, Sensor
from mysensors.sensor import ChildSensor
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
@@ -212,6 +212,8 @@ class MySensorsChildEntity(MySensorNodeEntity):
attr[ATTR_CHILD_ID] = self.child_id
attr[ATTR_DESCRIPTION] = self._child.description
# We should deprecate the battery level attribute in the future.
attr[ATTR_BATTERY_LEVEL] = self._node.battery_level
set_req = self.gateway.const.SetReq
for value_type, value in self._values.items():
@@ -58,7 +58,7 @@ def _get_stop_tags(
# Append directions for stops with shared titles
for tag, title in tags.items():
if title_counts[title] > 1:
tags[tag] = f"{title} ({stop_directions[tag]})"
tags[tag] = f"{title} ({stop_directions.get(tag, tag)})"
return tags
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nina",
"iot_class": "cloud_polling",
"loggers": ["pynina"],
"requirements": ["PyNINA==0.3.2"]
"requirements": ["PyNINA==0.3.3"]
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"services": {
"submit_movie_request": {
"name": "Sumbit movie request",
"name": "Submit movie request",
"description": "Searches for a movie and requests the first result.",
"fields": {
"name": {
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.0.35"]
"requirements": ["opower==0.0.36"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/philips_js",
"iot_class": "local_polling",
"loggers": ["haphilipsjs"],
"requirements": ["ha-philipsjs==3.1.0"]
"requirements": ["ha-philipsjs==3.1.1"]
}
@@ -259,7 +259,7 @@
"name": "DHW comfort mode"
},
"lock": {
"name": "[%key:component::lock::entity_component::_::name%]"
"name": "[%key:component::lock::title%]"
},
"relay": {
"name": "Relay"
+2 -1
View File
@@ -277,7 +277,8 @@ class MinutPointEntity(Entity):
sw_version=device["firmware"]["installed"],
via_device=(DOMAIN, device["home"]),
)
self._attr_name = f"{self._name} {device_class.capitalize()}"
if device_class:
self._attr_name = f"{self._name} {device_class.capitalize()}"
def __str__(self):
"""Return string representation of device."""
@@ -19,6 +19,30 @@ _LOGGER = logging.getLogger(__name__)
CONF_IRK = "irk"
def _parse_irk(irk: str) -> bytes | None:
if irk.startswith("irk:"):
irk = irk[4:]
if irk.endswith("="):
try:
irk_bytes = bytes(reversed(base64.b64decode(irk)))
except binascii.Error:
# IRK is not valid base64
return None
else:
try:
irk_bytes = binascii.unhexlify(irk)
except binascii.Error:
# IRK is not correctly hex encoded
return None
if len(irk_bytes) != 16:
# IRK must be 16 bytes when decoded
return None
return irk_bytes
class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BLE Device Tracker."""
@@ -35,15 +59,8 @@ class BLEDeviceTrackerConfigFlow(ConfigFlow, domain=DOMAIN):
if user_input is not None:
irk = user_input[CONF_IRK]
if irk.startswith("irk:"):
irk = irk[4:]
if irk.endswith("="):
irk_bytes = bytes(reversed(base64.b64decode(irk)))
else:
irk_bytes = binascii.unhexlify(irk)
if len(irk_bytes) != 16:
if not (irk_bytes := _parse_irk(irk)):
errors[CONF_IRK] = "irk_not_valid"
elif not (service_info := async_last_service_info(self.hass, irk_bytes)):
errors[CONF_IRK] = "irk_not_found"
@@ -6,5 +6,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/private_ble_device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.12.0"]
"requirements": ["bluetooth-data-tools==1.13.0"]
}
+1 -1
View File
@@ -3,5 +3,5 @@
"name": "Camera Proxy",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/proxy",
"requirements": ["Pillow==10.0.0"]
"requirements": ["Pillow==10.0.1"]
}
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
"iot_class": "local_polling",
"loggers": ["aioqsw"],
"requirements": ["aioqsw==0.3.4"]
"requirements": ["aioqsw==0.3.5"]
}
@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/qrcode",
"iot_class": "calculated",
"loggers": ["pyzbar"],
"requirements": ["Pillow==10.0.0", "pyzbar==0.1.7"]
"requirements": ["Pillow==10.0.1", "pyzbar==0.1.7"]
}
@@ -48,8 +48,11 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], BinarySensorE
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = f"{coordinator.device_name} Rainsensor"
@property
def is_on(self) -> bool | None:
+11 -6
View File
@@ -34,8 +34,9 @@ async def async_setup_entry(
[
RainBirdCalendarEntity(
data.schedule_coordinator,
data.coordinator.serial_number,
data.coordinator.unique_id,
data.coordinator.device_info,
data.coordinator.device_name,
)
]
)
@@ -47,20 +48,24 @@ class RainBirdCalendarEntity(
"""A calendar event entity."""
_attr_has_entity_name = True
_attr_name = None
_attr_name: str | None = None
_attr_icon = "mdi:sprinkler"
def __init__(
self,
coordinator: RainbirdScheduleUpdateCoordinator,
serial_number: str,
device_info: DeviceInfo,
unique_id: str | None,
device_info: DeviceInfo | None,
device_name: str,
) -> None:
"""Create the Calendar event device."""
super().__init__(coordinator)
self._event: CalendarEvent | None = None
self._attr_unique_id = serial_number
self._attr_device_info = device_info
if unique_id is not None:
self._attr_unique_id = unique_id
self._attr_device_info = device_info
else:
self._attr_name = device_name
@property
def event(self) -> CalendarEvent | None:
@@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import CONF_SERIAL_NUMBER, DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
from .const import DOMAIN, MANUFACTURER, TIMEOUT_SECONDS
UPDATE_INTERVAL = datetime.timedelta(minutes=1)
# The calendar data requires RPCs for each program/zone, and the data rarely
@@ -51,7 +51,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
hass: HomeAssistant,
name: str,
controller: AsyncRainbirdController,
serial_number: str,
unique_id: str | None,
model_info: ModelAndVersion,
) -> None:
"""Initialize RainbirdUpdateCoordinator."""
@@ -62,7 +62,7 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
update_interval=UPDATE_INTERVAL,
)
self._controller = controller
self._serial_number = serial_number
self._unique_id = unique_id
self._zones: set[int] | None = None
self._model_info = model_info
@@ -72,16 +72,23 @@ class RainbirdUpdateCoordinator(DataUpdateCoordinator[RainbirdDeviceState]):
return self._controller
@property
def serial_number(self) -> str:
"""Return the device serial number."""
return self._serial_number
def unique_id(self) -> str | None:
"""Return the config entry unique id."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
def device_name(self) -> str:
"""Device name for the rainbird controller."""
return f"{MANUFACTURER} Controller"
@property
def device_info(self) -> DeviceInfo | None:
"""Return information about the device."""
if self._unique_id is None:
return None
return DeviceInfo(
name=f"{MANUFACTURER} Controller",
identifiers={(DOMAIN, self._serial_number)},
name=self.device_name,
identifiers={(DOMAIN, self._unique_id)},
manufacturer=MANUFACTURER,
model=self._model_info.model_name,
sw_version=f"{self._model_info.major}.{self._model_info.minor}",
@@ -164,7 +171,7 @@ class RainbirdData:
self.hass,
name=self.entry.title,
controller=self.controller,
serial_number=self.entry.data[CONF_SERIAL_NUMBER],
unique_id=self.entry.unique_id,
model_info=self.model_info,
)
+5 -2
View File
@@ -51,8 +51,11 @@ class RainDelayNumber(CoordinatorEntity[RainbirdUpdateCoordinator], NumberEntity
) -> None:
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-rain-delay"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-rain-delay"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = f"{coordinator.device_name} Rain delay"
@property
def native_value(self) -> float | None:
+7 -2
View File
@@ -52,8 +52,13 @@ class RainBirdSensor(CoordinatorEntity[RainbirdUpdateCoordinator], SensorEntity)
"""Initialize the Rain Bird sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.serial_number}-{description.key}"
self._attr_device_info = coordinator.device_info
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{description.key}"
self._attr_device_info = coordinator.device_info
else:
self._attr_name = (
f"{coordinator.device_name} {description.key.capitalize()}"
)
@property
def native_value(self) -> StateType:
+12 -8
View File
@@ -65,20 +65,24 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity)
"""Initialize a Rain Bird Switch Device."""
super().__init__(coordinator)
self._zone = zone
_LOGGER.debug("coordinator.unique_id=%s", coordinator.unique_id)
if coordinator.unique_id is not None:
self._attr_unique_id = f"{coordinator.unique_id}-{zone}"
device_name = f"{MANUFACTURER} Sprinkler {zone}"
if imported_name:
self._attr_name = imported_name
self._attr_has_entity_name = False
else:
self._attr_name = None
self._attr_name = None if coordinator.unique_id is not None else device_name
self._attr_has_entity_name = True
self._duration_minutes = duration_minutes
self._attr_unique_id = f"{coordinator.serial_number}-{zone}"
self._attr_device_info = DeviceInfo(
name=f"{MANUFACTURER} Sprinkler {zone}",
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=MANUFACTURER,
via_device=(DOMAIN, coordinator.serial_number),
)
if coordinator.unique_id is not None and self._attr_unique_id is not None:
self._attr_device_info = DeviceInfo(
name=device_name,
identifiers={(DOMAIN, self._attr_unique_id)},
manufacturer=MANUFACTURER,
via_device=(DOMAIN, coordinator.unique_id),
)
@property
def extra_state_attributes(self):
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["vehicle==1.0.1"]
"requirements": ["vehicle==2.0.0"]
}
+59 -59
View File
@@ -526,7 +526,7 @@ def _compile_statistics(
):
continue
compiled: PlatformCompiledStatistics = platform_compile_statistics(
instance.hass, start, end
instance.hass, session, start, end
)
_LOGGER.debug(
"Statistics for %s during %s-%s: %s",
@@ -1871,7 +1871,7 @@ def get_latest_short_term_statistics_by_ids(
return list(
cast(
Sequence[Row],
execute_stmt_lambda_element(session, stmt, orm_rows=False),
execute_stmt_lambda_element(session, stmt),
)
)
@@ -1887,69 +1887,69 @@ def _latest_short_term_statistics_by_ids_stmt(
)
def get_latest_short_term_statistics(
def get_latest_short_term_statistics_with_session(
hass: HomeAssistant,
session: Session,
statistic_ids: set[str],
types: set[Literal["last_reset", "max", "mean", "min", "state", "sum"]],
metadata: dict[str, tuple[int, StatisticMetaData]] | None = None,
) -> dict[str, list[StatisticsRow]]:
"""Return the latest short term statistics for a list of statistic_ids."""
with session_scope(hass=hass, read_only=True) as session:
# Fetch metadata for the given statistic_ids
if not metadata:
metadata = get_instance(hass).statistics_meta_manager.get_many(
session, statistic_ids=statistic_ids
)
if not metadata:
return {}
metadata_ids = set(
_extract_metadata_and_discard_impossible_columns(metadata, types)
"""Return the latest short term statistics for a list of statistic_ids with a session."""
# Fetch metadata for the given statistic_ids
if not metadata:
metadata = get_instance(hass).statistics_meta_manager.get_many(
session, statistic_ids=statistic_ids
)
run_cache = get_short_term_statistics_run_cache(hass)
# Try to find the latest short term statistics ids for the metadata_ids
# from the run cache first if we have it. If the run cache references
# a non-existent id because of a purge, we will detect it missing in the
# next step and run a query to re-populate the cache.
stats: list[Row] = []
if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids):
stats = get_latest_short_term_statistics_by_ids(
session, metadata_id_to_id.values()
)
# If we are missing some metadata_ids in the run cache, we need run a query
# to populate the cache for each metadata_id, and then run another query
# to get the latest short term statistics for the missing metadata_ids.
if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and (
found_latest_ids := {
latest_id
for metadata_id in missing_metadata_ids
if (
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
run_cache, session, metadata_id
)
if not metadata:
return {}
metadata_ids = set(
_extract_metadata_and_discard_impossible_columns(metadata, types)
)
run_cache = get_short_term_statistics_run_cache(hass)
# Try to find the latest short term statistics ids for the metadata_ids
# from the run cache first if we have it. If the run cache references
# a non-existent id because of a purge, we will detect it missing in the
# next step and run a query to re-populate the cache.
stats: list[Row] = []
if metadata_id_to_id := run_cache.get_latest_ids(metadata_ids):
stats = get_latest_short_term_statistics_by_ids(
session, metadata_id_to_id.values()
)
# If we are missing some metadata_ids in the run cache, we need run a query
# to populate the cache for each metadata_id, and then run another query
# to get the latest short term statistics for the missing metadata_ids.
if (missing_metadata_ids := metadata_ids - set(metadata_id_to_id)) and (
found_latest_ids := {
latest_id
for metadata_id in missing_metadata_ids
if (
latest_id := cache_latest_short_term_statistic_id_for_metadata_id(
run_cache,
session,
metadata_id,
)
is not None
}
):
stats.extend(
get_latest_short_term_statistics_by_ids(session, found_latest_ids)
)
is not None
}
):
stats.extend(get_latest_short_term_statistics_by_ids(session, found_latest_ids))
if not stats:
return {}
if not stats:
return {}
# Return statistics combined with metadata
return _sorted_statistics_to_dict(
hass,
session,
stats,
statistic_ids,
metadata,
False,
StatisticsShortTerm,
None,
None,
types,
)
# Return statistics combined with metadata
return _sorted_statistics_to_dict(
hass,
session,
stats,
statistic_ids,
metadata,
False,
StatisticsShortTerm,
None,
None,
types,
)
def _generate_statistics_at_time_stmt(
@@ -2326,7 +2326,9 @@ def get_short_term_statistics_run_cache(
def cache_latest_short_term_statistic_id_for_metadata_id(
run_cache: ShortTermStatisticsRunCache, session: Session, metadata_id: int
run_cache: ShortTermStatisticsRunCache,
session: Session,
metadata_id: int,
) -> int | None:
"""Cache the latest short term statistic for a given metadata_id.
@@ -2337,9 +2339,7 @@ def cache_latest_short_term_statistic_id_for_metadata_id(
if latest := cast(
Sequence[Row],
execute_stmt_lambda_element(
session,
_find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id),
orm_rows=False,
session, _find_latest_short_term_statistic_for_metadata_id_stmt(metadata_id)
),
):
id_: int = latest[0].id
@@ -16,7 +16,7 @@
},
"complete_task": {
"name": "Complete task",
"description": "Completes a tasks that was privously created.",
"description": "Completes a task that was previously created.",
"fields": {
"id": {
"name": "ID",
@@ -95,6 +95,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
try:
return await host.api.check_new_firmware()
except (ReolinkError, asyncio.exceptions.CancelledError) as err:
task = asyncio.current_task()
if task is not None:
task.uncancel()
if starting:
_LOGGER.debug(
"Error checking Reolink firmware update at startup "

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