Compare commits

...

163 Commits

Author SHA1 Message Date
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
Franck Nijhof 04829f0a1b Bumped version to 2023.10.0b3 2023-09-30 10:54:01 +02:00
Raman Gupta 01182e8a5c Fix zwave_js firmware update logic (#101143)
* Fix zwave_js firmware update logic

* add comment

* tweak implementation for ssame outcome
2023-09-30 10:53:50 +02:00
Joost Lekkerkerker af041d2900 Bump aiowaqi to 1.1.1 (#101129) 2023-09-30 10:53:46 +02:00
Joost Lekkerkerker 822af4d40d Return None when value is not known in OpenHardwareMonitor (#101127)
* Return None when value is not known

* Add to coverage
2023-09-30 10:53:43 +02:00
Franck Nijhof 730acb34f2 Revert pin on AlexaPy (#101123) 2023-09-30 10:53:39 +02:00
Joost Lekkerkerker b5eb158697 Correct youtube stream selector in media extractor (#101119) 2023-09-30 10:53:36 +02:00
Joost Lekkerkerker d216fbddae Add logging to media extractor to know the selected stream (#101117) 2023-09-30 10:53:32 +02:00
Joost Lekkerkerker d84d83a42a Migrate WAQI unique id (#101112)
* Migrate unique_id

* Add docstring
2023-09-30 10:53:29 +02:00
Franck Nijhof c1ade85d65 Pin charset-normalizer in our package constraints (#101107) 2023-09-30 10:53:25 +02:00
Franck Nijhof 124eda6906 Correct binary ignore for charset-normalizer to charset_normalizer (#101106) 2023-09-30 10:53:22 +02:00
Stefan Agner 65c7b30720 Stop the Home Assistant Core container by default (#101105) 2023-09-30 10:53:18 +02:00
Franck Nijhof 1d2c570a01 Ignore binary distribution wheels for charset-normalizer (#101104) 2023-09-30 10:53:15 +02:00
Marc Mueller 2cc229ce42 Fix circular dependency on homeassistant (#101099) 2023-09-30 10:53:12 +02:00
Franck Nijhof 73356ae232 Use pep 503 compatible wheels index for builds (#101096) 2023-09-30 10:53:09 +02:00
Franck Nijhof bfd7275972 Update Home Assistant base image to 2023.09.0 (#101092) 2023-09-30 10:53:05 +02:00
jjlawren ef3bd0100c Bump plexapi to 4.15.3 (#101088)
* Bump plexapi to 4.15.3

* Update tests for updated account endpoint

* Update tests for updated resources endpoint

* Switch to non-web client fixture

* Set __qualname__ attribute for new library behavior
2023-09-30 10:53:01 +02:00
TheJulianJES 3f57c33f32 Fix ZHA exception when writing cie_addr during configuration (#101087)
Fix ZHA exception when writing `cie_addr`
2023-09-30 10:52:58 +02:00
Matthias Alphart bae3379938 Update xknxproject to 3.3.0 (#101081) 2023-09-30 10:52:55 +02:00
Michael Hansen 85838c6af9 Use wake word description if available (#101079) 2023-09-30 10:52:52 +02:00
TJ Horner 9c0bc57fed Add native precipitation unit for weatherkit (#101073) 2023-09-30 10:52:48 +02:00
tronikos 97448eff8f Bump opower to 0.0.35 (#101072) 2023-09-30 10:52:42 +02:00
Paul Bottein 2f6fefefa7 Update frontend to 20230928.0 (#101067) 2023-09-30 10:52:36 +02:00
Franck Nijhof d8f96d7709 Bumped version to 2023.10.0b2 2023-09-28 20:05:38 +02:00
Joost Lekkerkerker fff3c6c6e9 Bump aiowaqi to 1.1.0 (#99751)
* Bump aiowaqi to 1.1.0

* Fix hassfest

* Fix tests
2023-09-28 20:03:43 +02:00
Michael Hansen 17362e1954 Remove fma instructions from webrtc-noise-gain (#101060) 2023-09-28 20:03:21 +02:00
TJ Horner 1bbd4662b7 Bump apple_weatherkit to 1.0.4 (#101057) 2023-09-28 20:03:18 +02:00
Joost Lekkerkerker ad8033c0f2 Don't show withings repair if it's not in YAML (#101054) 2023-09-28 20:03:15 +02:00
Álvaro Fernández Rojas 081f194f6a Update aioairzone-cloud to v0.2.3 (#101052)
Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2023-09-28 20:03:12 +02:00
Joost Lekkerkerker f13059eaf5 Pin pydantic to 1.10.12 (#101044) 2023-09-28 20:03:08 +02:00
Joost Lekkerkerker 0147108b89 Fix onvif creating a new entity for every new event (#101035)
Use topic value as topic
2023-09-28 20:03:05 +02:00
Jesse Hills ffad30734b ESPHome: dont send error when wake word is aborted (#101032)
* ESPHome dont send error when wake word is aborted

* Add test
2023-09-28 20:03:02 +02:00
Erik Montnemery 5bd306392f Add LED control support to Home Assistant Green (#100922)
* Add LED control support to Home Assistant Green

* Add strings.json

* Sort alphabetically

* Reorder LED schema

* Improve test coverage

* Apply suggestions from code review

Co-authored-by: Stefan Agner <stefan@agner.ch>

* Sort + fix test

* Remove reboot menu

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2023-09-28 20:02:59 +02:00
Tereza Tomcova d6c42ee8e7 Bump PySwitchbot to 0.40.0 to support Curtain 3 (#100619) 2023-09-28 20:02:55 +02:00
Joost Lekkerkerker 35eaebd182 Add feature to add measuring station via number in waqi (#99992)
* Add feature to add measuring station via number

* Add feature to add measuring station via number

* Add feature to add measuring station via number
2023-09-28 20:02:52 +02:00
tyjtyj 81e8ca130f Fix google maps device_tracker same last seen timestamp (#99963)
* Update device_tracker.py 

This fix the google_maps does not show current location when HA started/restarted and also fix unnecessary update when last_seen timestamp is the same. 
Unnecessary update is causing proximity sensor switching from between stationary and certain direction.

* Remove elif

* Fix Black check

* fix black check

* Update device_tracker.py

Better patch

* Update device_tracker.py

* Update device_tracker.py

Fix Black

* Update device_tracker.py

change warning to debug
2023-09-28 20:02:48 +02:00
lennart24 9ab340047d Add shutter_tilt support for Fibaro FGR 223 (#96283)
* add support for shutter_tilt for Fibaro FGR 223
add tests for fgr 223

* Adjust comments and docstring

---------

Co-authored-by: Lennart <18117505+Ced4@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2023-09-28 19:50:52 +02:00
Paulus Schoutsen b02f64196b Bumped version to 2023.10.0b1 2023-09-27 21:00:57 -04:00
Michael Hansen af37de46bd Use webrtc-noise-gain without AVX2 (#101028) 2023-09-27 21:00:50 -04:00
Marc Mueller be93793db9 Update pyweatherflowudp to 1.4.3 (#101022) 2023-09-27 21:00:49 -04:00
J. Nick Koston c287564e68 Fix HomeKit handling of unavailable state (#101021) 2023-09-27 21:00:48 -04:00
Jan Bouwhuis 115c3d6e49 Fix handling reload with invalid mqtt config (#101015)
Fix handling reload whith invalid mqtt config
2023-09-27 21:00:47 -04:00
Marcel van der Veldt 415042f356 Adopt Hue integration to latest changes in Hue firmware (#101001) 2023-09-27 21:00:46 -04:00
steffenrapp dde4b07c29 Add homeassistant reload_all translatable service name and description (#100437)
* Update services.yaml

* Update strings.json

* Update strings.json
2023-09-27 21:00:45 -04:00
Jan-Philipp Benecke 10e8173d4e Restore state of trend sensor (#100332)
* Restoring state of trend sensor

* Handle unknown state & parametrize tests
2023-09-27 21:00:44 -04:00
293 changed files with 6476 additions and 4625 deletions
+1
View File
@@ -898,6 +898,7 @@ omit =
homeassistant/components/opengarage/cover.py
homeassistant/components/opengarage/entity.py
homeassistant/components/opengarage/sensor.py
homeassistant/components/openhardwaremonitor/sensor.py
homeassistant/components/openhome/__init__.py
homeassistant/components/openhome/const.py
homeassistant/components/openhome/media_player.py
+3 -3
View File
@@ -186,7 +186,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
@@ -200,7 +200,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
@@ -214,7 +214,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;grpcio;SQLAlchemy;protobuf
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"
-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
+3 -6
View File
@@ -15,9 +15,8 @@ COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
RUN \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--find-links "${WHEELS_LINKS}" \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
-r homeassistant/requirements.txt
COPY requirements_all.txt home_assistant_frontend-* home_assistant_intents-* homeassistant/
@@ -39,9 +38,8 @@ RUN \
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--find-links "${WHEELS_LINKS}" \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
-r homeassistant/requirements_all.txt
## Setup Home Assistant Core
@@ -49,9 +47,8 @@ COPY . homeassistant/
RUN \
pip3 install \
--no-cache-dir \
--no-index \
--only-binary=:all: \
--find-links "${WHEELS_LINKS}" \
--index-url "https://wheels.home-assistant.io/musllinux-index/" \
-e ./homeassistant \
&& python3 -m compileall \
homeassistant/homeassistant
+5 -5
View File
@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.08.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.08.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.08.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.08.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.08.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2023.09.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2023.09.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2023.09.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2023.09.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2023.09.0
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io
@@ -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."""
+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
)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_polling",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.2.2"]
"requirements": ["aioairzone-cloud==0.2.3"]
}
@@ -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
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["webrtc-noise-gain==1.2.1"]
"requirements": ["webrtc-noise-gain==1.2.3"]
}
@@ -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": {
@@ -19,6 +19,6 @@
"bluetooth-adapters==0.16.1",
"bluetooth-auto-recovery==1.2.3",
"bluetooth-data-tools==1.12.0",
"dbus-fast==2.11.0"
"dbus-fast==2.11.1"
]
}
@@ -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:
+16 -3
View File
@@ -528,19 +528,24 @@ class CalendarEntity(Entity):
the current or upcoming event.
"""
super().async_write_ha_state()
_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,6 +558,13 @@ 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.
@@ -561,6 +573,7 @@ class CalendarEntity(Entity):
"""
for unsub in self._alarm_unsubs:
unsub()
self._alarm_unsubs.clear()
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.0"]
}
@@ -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"]
}
+7 -4
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,
@@ -16,7 +16,7 @@
"loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [
"async-interrupt==1.1.1",
"aioesphomeapi==17.0.0",
"aioesphomeapi==17.0.1",
"bluetooth-data-tools==1.12.0",
"esphome-dashboard-api==1.2.3"
],
@@ -24,7 +24,10 @@ from homeassistant.components.assist_pipeline import (
async_pipeline_from_audio_stream,
select as pipeline_select,
)
from homeassistant.components.assist_pipeline.error import WakeWordDetectionError
from homeassistant.components.assist_pipeline.error import (
WakeWordDetectionAborted,
WakeWordDetectionError,
)
from homeassistant.components.media_player import async_process_play_media_url
from homeassistant.core import Context, HomeAssistant, callback
@@ -219,7 +222,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 = (
@@ -257,6 +260,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),
),
)
@@ -273,6 +277,8 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
},
)
_LOGGER.warning("Pipeline not found")
except WakeWordDetectionAborted:
pass # Wake word detection was aborted and `handle_finished` is enough.
except WakeWordDetectionError as e:
self.handle_event(
VoiceAssistantEventType.VOICE_ASSISTANT_ERROR,
@@ -281,7 +287,6 @@ class VoiceAssistantUDPServer(asyncio.DatagramProtocol):
"message": e.message,
},
)
_LOGGER.warning("No Wake word provider found")
finally:
self.handle_finished()
@@ -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==20230926.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,
)
@@ -112,12 +112,22 @@ class GoogleMapsScanner:
last_seen = dt_util.as_utc(person.datetime)
if last_seen < self._prev_seen.get(dev_id, last_seen):
_LOGGER.warning(
_LOGGER.debug(
"Ignoring %s update because timestamp is older than last timestamp",
person.nickname,
)
_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"
):
_LOGGER.debug(
"Ignoring %s update because timestamp "
"is the same as the last timestamp %s",
person.nickname,
last_seen,
)
continue
self._prev_seen[dev_id] = last_seen
attrs = {
@@ -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%]",
@@ -88,11 +88,13 @@ from .handler import ( # noqa: F401
async_get_addon_discovery_info,
async_get_addon_info,
async_get_addon_store_info,
async_get_green_settings,
async_get_yellow_settings,
async_install_addon,
async_reboot_host,
async_restart_addon,
async_set_addon_options,
async_set_green_settings,
async_set_yellow_settings,
async_start_addon,
async_stop_addon,
@@ -263,6 +263,27 @@ async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> b
return await hassio.send_command(command, timeout=None)
@api_data
async def async_get_green_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Green."""
hassio: HassIO = hass.data[DOMAIN]
return await hassio.send_command("/os/boards/green", method="get")
@api_data
async def async_set_green_settings(
hass: HomeAssistant, settings: dict[str, bool]
) -> dict:
"""Set settings specific to Home Assistant Green.
Returns an empty dict.
"""
hassio: HassIO = hass.data[DOMAIN]
return await hassio.send_command(
"/os/boards/green", method="post", payload=settings
)
@api_data
async def async_get_yellow_settings(hass: HomeAssistant) -> dict[str, bool]:
"""Return settings specific to Home Assistant Yellow."""
@@ -60,3 +60,5 @@ reload_config_entry:
text:
save_persistent_states:
reload_all:
@@ -125,6 +125,10 @@
"save_persistent_states": {
"name": "Save persistent states",
"description": "Saves the persistent states immediately. Maintains the normal periodic saving interval."
},
"reload_all": {
"name": "Reload all",
"description": "Reload all YAML configuration that can be reloaded without restarting Home Assistant."
}
}
}
@@ -1,22 +1,100 @@
"""Config flow for the Home Assistant Green integration."""
from __future__ import annotations
import asyncio
import logging
from typing import Any
from homeassistant.config_entries import ConfigFlow
import aiohttp
import voluptuous as vol
from homeassistant.components.hassio import (
HassioAPIError,
async_get_green_settings,
async_set_green_settings,
is_hassio,
)
from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers import selector
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
STEP_HW_SETTINGS_SCHEMA = vol.Schema(
{
# Sorted to match front panel left to right
vol.Required("power_led"): selector.BooleanSelector(),
vol.Required("activity_led"): selector.BooleanSelector(),
vol.Required("system_health_led"): selector.BooleanSelector(),
}
)
class HomeAssistantGreenConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Green."""
VERSION = 1
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> HomeAssistantGreenOptionsFlow:
"""Return the options flow."""
return HomeAssistantGreenOptionsFlow()
async def async_step_system(self, data: dict[str, Any] | None = None) -> FlowResult:
"""Handle the initial step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="Home Assistant Green", data={})
class HomeAssistantGreenOptionsFlow(OptionsFlow):
"""Handle an option flow for Home Assistant Green."""
_hw_settings: dict[str, bool] | None = None
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if not is_hassio(self.hass):
return self.async_abort(reason="not_hassio")
return await self.async_step_hardware_settings()
async def async_step_hardware_settings(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle hardware settings."""
if user_input is not None:
if self._hw_settings == user_input:
return self.async_create_entry(data={})
try:
async with asyncio.timeout(10):
await async_set_green_settings(self.hass, user_input)
except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err:
_LOGGER.warning("Failed to write hardware settings", exc_info=err)
return self.async_abort(reason="write_hw_settings_error")
return self.async_create_entry(data={})
try:
async with asyncio.timeout(10):
self._hw_settings: dict[str, bool] = await async_get_green_settings(
self.hass
)
except (aiohttp.ClientError, TimeoutError, HassioAPIError) as err:
_LOGGER.warning("Failed to read hardware settings", exc_info=err)
return self.async_abort(reason="read_hw_settings_error")
schema = self.add_suggested_values_to_schema(
STEP_HW_SETTINGS_SCHEMA, self._hw_settings
)
return self.async_show_form(step_id="hardware_settings", data_schema=schema)
@@ -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,
)
]
@@ -0,0 +1,28 @@
{
"options": {
"step": {
"hardware_settings": {
"title": "Configure hardware settings",
"data": {
"activity_led": "Green: activity LED",
"power_led": "White: power LED",
"system_health_led": "Yellow: system health LED"
}
},
"reboot_menu": {
"title": "Reboot required",
"description": "The settings have changed, but the new settings will not take effect until the system is rebooted",
"menu_options": {
"reboot_later": "Reboot manually later",
"reboot_now": "Reboot now"
}
}
},
"abort": {
"not_hassio": "[%key:component::homeassistant_hardware::silabs_multiprotocol_hardware::options::abort::not_hassio%]",
"read_hw_settings_error": "Failed to read hardware settings",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"write_hw_settings_error": "Failed to write hardware settings"
}
}
}
@@ -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."""
@@ -465,7 +465,9 @@ class HomeAccessory(Accessory): # type: ignore[misc]
def async_update_state_callback(self, new_state: State | None) -> None:
"""Handle state change listener callback."""
_LOGGER.debug("New_state: %s", new_state)
if new_state is None:
# HomeKit handles unavailable state via the available property
# so we should not propagate it here
if new_state is None or new_state.state == STATE_UNAVAILABLE:
return
battery_state = None
battery_charging_state = None
@@ -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"
+1 -1
View File
@@ -11,6 +11,6 @@
"iot_class": "local_push",
"loggers": ["aiohue"],
"quality_scale": "platinum",
"requirements": ["aiohue==4.6.2"],
"requirements": ["aiohue==4.7.0"],
"zeroconf": ["_hue._tcp.local."]
}
@@ -1,7 +1,7 @@
"""Support for Hue binary sensors."""
from __future__ import annotations
from typing import Any, TypeAlias
from typing import TypeAlias
from aiohue.v2 import HueBridgeV2
from aiohue.v2.controllers.config import (
@@ -9,9 +9,17 @@ from aiohue.v2.controllers.config import (
EntertainmentConfigurationController,
)
from aiohue.v2.controllers.events import EventType
from aiohue.v2.controllers.sensors import MotionController
from aiohue.v2.controllers.sensors import (
CameraMotionController,
ContactController,
MotionController,
TamperController,
)
from aiohue.v2.models.camera_motion import CameraMotion
from aiohue.v2.models.contact import Contact, ContactState
from aiohue.v2.models.entertainment_configuration import EntertainmentStatus
from aiohue.v2.models.motion import Motion
from aiohue.v2.models.tamper import Tamper, TamperState
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
@@ -25,8 +33,16 @@ from ..bridge import HueBridge
from ..const import DOMAIN
from .entity import HueBaseEntity
SensorType: TypeAlias = Motion | EntertainmentConfiguration
ControllerType: TypeAlias = MotionController | EntertainmentConfigurationController
SensorType: TypeAlias = (
CameraMotion | Contact | Motion | EntertainmentConfiguration | Tamper
)
ControllerType: TypeAlias = (
CameraMotionController
| ContactController
| MotionController
| EntertainmentConfigurationController
| TamperController
)
async def async_setup_entry(
@@ -57,8 +73,11 @@ async def async_setup_entry(
)
# setup for each binary-sensor-type hue resource
register_items(api.sensors.camera_motion, HueMotionSensor)
register_items(api.sensors.motion, HueMotionSensor)
register_items(api.config.entertainment_configuration, HueEntertainmentActiveSensor)
register_items(api.sensors.contact, HueContactSensor)
register_items(api.sensors.tamper, HueTamperSensor)
class HueBinarySensorBase(HueBaseEntity, BinarySensorEntity):
@@ -87,12 +106,7 @@ class HueMotionSensor(HueBinarySensorBase):
if not self.resource.enabled:
# Force None (unknown) if the sensor is set to disabled in Hue
return None
return self.resource.motion.motion
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {"motion_valid": self.resource.motion.motion_valid}
return self.resource.motion.value
class HueEntertainmentActiveSensor(HueBinarySensorBase):
@@ -110,3 +124,30 @@ class HueEntertainmentActiveSensor(HueBinarySensorBase):
"""Return sensor name."""
type_title = self.resource.type.value.replace("_", " ").title()
return f"{self.resource.metadata.name}: {type_title}"
class HueContactSensor(HueBinarySensorBase):
"""Representation of a Hue Contact sensor."""
_attr_device_class = BinarySensorDeviceClass.OPENING
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if not self.resource.enabled:
# Force None (unknown) if the sensor is set to disabled in Hue
return None
return self.resource.contact_report.state != ContactState.CONTACT
class HueTamperSensor(HueBinarySensorBase):
"""Representation of a Hue Tamper sensor."""
_attr_device_class = BinarySensorDeviceClass.TAMPER
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
if not self.resource.tamper_reports:
return False
return self.resource.tamper_reports[0].state == TamperState.TAMPERED
+5 -9
View File
@@ -100,12 +100,7 @@ class HueTemperatureSensor(HueSensorBase):
@property
def native_value(self) -> float:
"""Return the value reported by the sensor."""
return round(self.resource.temperature.temperature, 1)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {"temperature_valid": self.resource.temperature.temperature_valid}
return round(self.resource.temperature.value, 1)
class HueLightLevelSensor(HueSensorBase):
@@ -122,14 +117,13 @@ class HueLightLevelSensor(HueSensorBase):
# scale used because the human eye adjusts to light levels and small
# changes at low lux levels are more noticeable than at high lux
# levels.
return int(10 ** ((self.resource.light.light_level - 1) / 10000))
return int(10 ** ((self.resource.light.value - 1) / 10000))
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
return {
"light_level": self.resource.light.light_level,
"light_level_valid": self.resource.light.light_level_valid,
"light_level": self.resource.light.value,
}
@@ -149,6 +143,8 @@ class HueBatterySensor(HueSensorBase):
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
if self.resource.power_state.battery_state is None:
return {}
return {"battery_state": self.resource.power_state.battery_state.value}
@@ -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"]
}
+1 -1
View File
@@ -12,7 +12,7 @@
"quality_scale": "platinum",
"requirements": [
"xknx==2.11.2",
"xknxproject==3.2.0",
"xknxproject==3.3.0",
"knx-frontend==2023.6.23.191712"
]
}
@@ -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"]
}
+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()
@@ -153,7 +153,7 @@ class MediaExtractor:
except MEQueryException:
_LOGGER.error("Wrong query format: %s", stream_query)
return
_LOGGER.debug("Selected the following stream: %s", stream_url)
data = {k: v for k, v in self.call_data.items() if k != ATTR_ENTITY_ID}
data[ATTR_MEDIA_CONTENT_ID] = stream_url
@@ -193,9 +193,16 @@ def get_best_stream(formats: list[dict[str, Any]]) -> str:
def get_best_stream_youtube(formats: list[dict[str, Any]]) -> str:
"""YouTube requests also include manifest files.
"""YouTube responses also include files with only video or audio.
They don't have a filesize so we skip all formats without filesize.
So we filter on files with both audio and video codec.
"""
return get_best_stream([format for format in formats if "filesize" in format])
return get_best_stream(
[
format
for format in formats
if format.get("acodec", "none") != "none"
and format.get("vcodec", "none") != "none"
]
)
+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}"
+10 -3
View File
@@ -24,7 +24,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HassJob, HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import TemplateError, Unauthorized
from homeassistant.exceptions import HomeAssistantError, TemplateError, Unauthorized
from homeassistant.helpers import config_validation as cv, event as ev, template
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -364,8 +364,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _reload_config(call: ServiceCall) -> None:
"""Reload the platforms."""
# Fetch updated manual configured items and validate
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
# Fetch updated manually configured items and validate
if (
config_yaml := await async_integration_yaml_config(hass, DOMAIN)
) is None:
# Raise in case we have an invalid configuration
raise HomeAssistantError(
"Error reloading manually configured MQTT items, "
"check your configuration.yaml"
)
mqtt_data.config = config_yaml.get(DOMAIN, {})
# Reload the modern yaml platforms
@@ -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?
@@ -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
@@ -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": {
+125 -99
View File
@@ -48,15 +48,16 @@ async def async_parse_motion_alarm(uid: str, msg) -> Event | None:
Topic: tns1:VideoSource/MotionAlarm
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Motion Alarm",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -71,15 +72,16 @@ async def async_parse_image_too_blurry(uid: str, msg) -> Event | None:
Topic: tns1:VideoSource/ImageTooBlurry/*
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Image Too Blurry",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -95,15 +97,16 @@ async def async_parse_image_too_dark(uid: str, msg) -> Event | None:
Topic: tns1:VideoSource/ImageTooDark/*
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Image Too Dark",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -119,15 +122,16 @@ async def async_parse_image_too_bright(uid: str, msg) -> Event | None:
Topic: tns1:VideoSource/ImageTooBright/*
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Image Too Bright",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -143,15 +147,16 @@ async def async_parse_scene_change(uid: str, msg) -> Event | None:
Topic: tns1:VideoSource/GlobalSceneChange/*
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Global Scene Change",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -167,8 +172,9 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None:
audio_source = ""
audio_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "AudioSourceConfigurationToken":
audio_source = source.Value
if source.Name == "AudioAnalyticsConfigurationToken":
@@ -177,12 +183,12 @@ async def async_parse_detected_sound(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{audio_source}_{audio_analytics}_{rule}",
f"{uid}_{topic_value}_{audio_source}_{audio_analytics}_{rule}",
"Detected Sound",
"binary_sensor",
"sound",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -198,8 +204,9 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_video_source(source.Value)
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -208,12 +215,12 @@ async def async_parse_field_detector(uid: str, msg) -> Event | None:
rule = source.Value
evt = Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Field Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
return evt
except (AttributeError, KeyError):
@@ -230,8 +237,9 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_video_source(source.Value)
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -240,12 +248,12 @@ async def async_parse_cell_motion_detector(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Cell Motion Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -261,8 +269,9 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_video_source(source.Value)
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -271,12 +280,12 @@ async def async_parse_motion_region_detector(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Motion Region Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value in ["1", "true"],
message_value.Data.SimpleItem[0].Value in ["1", "true"],
)
except (AttributeError, KeyError):
return None
@@ -292,8 +301,9 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_video_source(source.Value)
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -302,12 +312,12 @@ async def async_parse_tamper_detector(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Tamper Detection",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -322,18 +332,19 @@ async def async_parse_dog_cat_detector(uid: str, msg) -> Event | None:
"""
try:
video_source = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{value_1}_{video_source}",
f"{uid}_{topic_value}_{video_source}",
"Pet Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -347,18 +358,19 @@ async def async_parse_vehicle_detector(uid: str, msg) -> Event | None:
"""
try:
video_source = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{value_1}_{video_source}",
f"{uid}_{topic_value}_{video_source}",
"Vehicle Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -372,18 +384,19 @@ async def async_parse_person_detector(uid: str, msg) -> Event | None:
"""
try:
video_source = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{value_1}_{video_source}",
f"{uid}_{topic_value}_{video_source}",
"Person Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -397,18 +410,19 @@ async def async_parse_face_detector(uid: str, msg) -> Event | None:
"""
try:
video_source = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{value_1}_{video_source}",
f"{uid}_{topic_value}_{video_source}",
"Face Detection",
"binary_sensor",
"motion",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -422,18 +436,19 @@ async def async_parse_visitor_detector(uid: str, msg) -> Event | None:
"""
try:
video_source = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "Source":
video_source = _normalize_video_source(source.Value)
return Event(
f"{uid}_{value_1}_{video_source}",
f"{uid}_{topic_value}_{video_source}",
"Visitor Detection",
"binary_sensor",
"occupancy",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -446,15 +461,16 @@ async def async_parse_digital_input(uid: str, msg) -> Event | None:
Topic: tns1:Device/Trigger/DigitalInput
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Digital Input",
"binary_sensor",
None,
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
)
except (AttributeError, KeyError):
return None
@@ -467,15 +483,16 @@ async def async_parse_relay(uid: str, msg) -> Event | None:
Topic: tns1:Device/Trigger/Relay
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Relay Triggered",
"binary_sensor",
None,
None,
value_1.Data.SimpleItem[0].Value == "active",
message_value.Data.SimpleItem[0].Value == "active",
)
except (AttributeError, KeyError):
return None
@@ -488,15 +505,16 @@ async def async_parse_storage_failure(uid: str, msg) -> Event | None:
Topic: tns1:Device/HardwareFailure/StorageFailure
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Storage Failure",
"binary_sensor",
"problem",
None,
value_1.Data.SimpleItem[0].Value == "true",
message_value.Data.SimpleItem[0].Value == "true",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -510,13 +528,14 @@ async def async_parse_processor_usage(uid: str, msg) -> Event | None:
Topic: tns1:Monitoring/ProcessorUsage
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
usage = float(value_1.Data.SimpleItem[0].Value)
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
usage = float(message_value.Data.SimpleItem[0].Value)
if usage <= 1:
usage *= 100
return Event(
f"{uid}_{value_1}",
f"{uid}_{topic_value}",
"Processor Usage",
"sensor",
None,
@@ -535,10 +554,11 @@ async def async_parse_last_reboot(uid: str, msg) -> Event | None:
Topic: tns1:Monitoring/OperatingTime/LastReboot
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
return Event(
f"{uid}_{value_1}",
f"{uid}_{topic_value}",
"Last Reboot",
"sensor",
"timestamp",
@@ -557,10 +577,11 @@ async def async_parse_last_reset(uid: str, msg) -> Event | None:
Topic: tns1:Monitoring/OperatingTime/LastReset
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
return Event(
f"{uid}_{value_1}",
f"{uid}_{topic_value}",
"Last Reset",
"sensor",
"timestamp",
@@ -581,10 +602,11 @@ async def async_parse_backup_last(uid: str, msg) -> Event | None:
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
return Event(
f"{uid}_{value_1}",
f"{uid}_{topic_value}",
"Last Backup",
"sensor",
"timestamp",
@@ -604,10 +626,11 @@ async def async_parse_last_clock_sync(uid: str, msg) -> Event | None:
Topic: tns1:Monitoring/OperatingTime/LastClockSynchronization
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(value_1.Data.SimpleItem[0].Value)
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
date_time = local_datetime_or_none(message_value.Data.SimpleItem[0].Value)
return Event(
f"{uid}_{value_1}",
f"{uid}_{topic_value}",
"Last Clock Synchronization",
"sensor",
"timestamp",
@@ -628,15 +651,16 @@ async def async_parse_jobstate(uid: str, msg) -> Event | None:
"""
try:
value_1 = msg.Message._value_1 # pylint: disable=protected-access
source = value_1.Source.SimpleItem[0].Value
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
source = message_value.Source.SimpleItem[0].Value
return Event(
f"{uid}_{value_1}_{source}",
f"{uid}_{topic_value}_{source}",
"Recording Job State",
"binary_sensor",
None,
None,
value_1.Data.SimpleItem[0].Value == "Active",
message_value.Data.SimpleItem[0].Value == "Active",
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -653,8 +677,9 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = source.Value
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -663,12 +688,12 @@ async def async_parse_linedetector_crossed(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Line Detector Crossed",
"sensor",
None,
None,
value_1.Data.SimpleItem[0].Value,
message_value.Data.SimpleItem[0].Value,
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -685,8 +710,9 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
video_source = ""
video_analytics = ""
rule = ""
value_1 = msg.Message._value_1 # pylint: disable=protected-access
for source in value_1.Source.SimpleItem:
message_value = msg.Message._value_1 # pylint: disable=protected-access
topic_value = msg.Topic._value_1 # pylint: disable=protected-access
for source in message_value.Source.SimpleItem:
if source.Name == "VideoSourceConfigurationToken":
video_source = _normalize_video_source(source.Value)
if source.Name == "VideoAnalyticsConfigurationToken":
@@ -695,12 +721,12 @@ async def async_parse_count_aggregation_counter(uid: str, msg) -> Event | None:
rule = source.Value
return Event(
f"{uid}_{value_1}_{video_source}_{video_analytics}_{rule}",
f"{uid}_{topic_value}_{video_source}_{video_analytics}_{rule}",
"Count Aggregation Counter",
"sensor",
None,
None,
value_1.Data.SimpleItem[0].Value,
message_value.Data.SimpleItem[0].Value,
EntityCategory.DIAGNOSTIC,
)
except (AttributeError, KeyError):
@@ -79,6 +79,8 @@ class OpenHardwareMonitorDevice(SensorEntity):
@property
def native_value(self):
"""Return the state of the device."""
if self.value == "-":
return None
return self.value
@property
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/opower",
"iot_class": "cloud_polling",
"loggers": ["opower"],
"requirements": ["opower==0.0.34"]
"requirements": ["opower==0.0.35"]
}
@@ -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"]
}
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "local_push",
"loggers": ["plexapi", "plexwebsocket"],
"requirements": [
"PlexAPI==4.13.2",
"PlexAPI==4.15.3",
"plexauth==0.0.6",
"plexwebsocket==0.0.13"
],
@@ -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"

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