Compare commits

..

149 Commits

Author SHA1 Message Date
Franck Nijhof b4f1683c40 2022.9.6 (#78916) 2022-09-22 10:29:12 +02:00
rikroe 7be5fde8d6 Bump bimmer_connected to 0.10.4 (#78910)
Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2022-09-22 08:55:03 +02:00
J. Nick Koston d44ff16f9d Handle timeout fetching bond token in config flow (#78896) 2022-09-22 08:55:00 +02:00
J. Nick Koston b51dc0884e Fix samsungtv to abort when ATTR_UPNP_MANUFACTURER is missing (#78895) 2022-09-22 08:54:56 +02:00
Jan Bouwhuis f3451858ef Correct return typing for catch_log_exception (#78399) 2022-09-22 08:54:42 +02:00
Paulus Schoutsen 103f490519 Bumped version to 2022.9.6 2022-09-21 22:33:08 -04:00
Paulus Schoutsen 68fa40c0fa Disable force update Netatmo (#78913) 2022-09-21 22:31:36 -04:00
J. Nick Koston 5c294550e8 Handle default RSSI values from bleak in bluetooth (#78908) 2022-09-21 22:30:41 -04:00
Michael 72769130f9 Check Surveillance Station permissions during setup of Synology DSM integration (#78884) 2022-09-21 22:30:40 -04:00
Jc2k 8f21e7775b Fix parsing Eve Energy characteristic data (#78880) 2022-09-21 22:30:40 -04:00
Marc Mueller 6704efd1ef Pin Python patch versions [ci] (#78830) 2022-09-21 22:29:25 -04:00
Joakim Plate 829777a211 If brightness is not available, don't set a value (#78827) 2022-09-21 22:25:42 -04:00
J. Nick Koston 48c6fbf22e Bump dbus-fast to 1.5.1 (#78802) 2022-09-21 22:25:42 -04:00
Aaron Bach fac2a46781 Guard Guardian switches from redundant on/off calls (#78791) 2022-09-21 22:25:13 -04:00
Aaron Bach a688b4c581 Fix bug wherein RainMachine services use the wrong controller (#78780) 2022-09-21 22:21:29 -04:00
Nico 8c9e0a8239 Bump aioimaplib to 1.0.1 (#78738) 2022-09-21 22:21:28 -04:00
J. Nick Koston 91398b6a75 Drop PARALLEL_UPDATES from switchbot (#78713) 2022-09-21 22:21:28 -04:00
Jan Bouwhuis dea221b155 Link manually added MQTT entities the the MQTT config entry (#78547)
Co-authored-by: Erik <erik@montnemery.com>
2022-09-21 22:20:56 -04:00
starkillerOG bfcb333227 Add status codes 23 and 26 to Xiaomi Miio vacuum (#78289)
* Add status codes 23 and 26

* change status 26
2022-09-21 22:15:47 -04:00
Jan Bouwhuis 3dd0dbf38f Make hass.data["mqtt"] an instance of a DataClass (#77972)
* Use dataclass to reference hass.data globals

* Add discovery_registry_hooks to dataclass

* Move discovery registry hooks to dataclass

* Add device triggers to dataclass

* Cleanup DEVICE_TRIGGERS const

* Add last_discovery to data_class

* Simplify typing for class `Subscription`

* Follow up on comment

* Redo suggested typing change to sasisfy mypy

* Restore typing

* Add mypy version to CI check logging

* revert changes to ci.yaml

* Add docstr for protocol

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>

* Mypy update after merging #78399

* Remove mypy ignore

* Correct return type

Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
2022-09-21 22:15:47 -04:00
Jan Bouwhuis 4894e2e5a4 Refactor common MQTT tests to use modern schema (#77583)
* Common tests availability

* Common tests attributes

* Common tests unique id

* Common tests discovery

* Common tests encoding

* Common tests device info

* Common tests entity_id updated

* Common tests entity debug info

* Common test entity category

* Common tests setup reload unload+corrections

* Cleanup sweep

* Comments from curent change

* Cleanup

* Remove unused legacy config
2022-09-21 22:15:46 -04:00
Jan Bouwhuis e7a616e8e4 Refactor MQTT tests to use modern platform schema part 2 (#77525)
* Tests light json

* Tests light template

* Missed test light json

* Tests light

* Tests lock

* Tests number

* Tests scene

* Tests select

* Tests sensor

* Tests siren

* Tests state vacuuum

* Tests switch

* Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG

* Suggested comment changes
2022-09-21 22:15:46 -04:00
Jan Bouwhuis b3a4838978 Refactor MQTT tests to use modern platform schema part 1 (#77387)
* Tests alarm_control_panel

* Tests binary_sensor

* Tests button

* Tests camera

* Tests Climate + corrections default config

* Tests cover

* Tests device_tracker

* Tests fan

* Tests humidifier

* Fix test_supported_features test fan

* Tests init

* Tests legacy vacuum

* Derive DEFAULT_CONFIG_LEGACY from DEFAULT_CONFIG

* Commit suggestion comment changes
2022-09-21 22:15:45 -04:00
J. Nick Koston 933dde1d1e Handle Modalias missing from the bluetooth adapter details on older BlueZ (#78716) 2022-09-18 21:10:05 -04:00
Paulus Schoutsen a411cd9c20 2022.9.5 (#78703) 2022-09-18 14:05:34 -04:00
Paulus Schoutsen da81dbe6ac Bumped version to 2022.9.5 2022-09-18 13:13:54 -04:00
J. Nick Koston f5c30ab10a Bump thermobeacon-ble to 0.3.2 (#78693) 2022-09-18 13:13:48 -04:00
J. Nick Koston 454675d86b Fix bluetooth callback matchers when only matching on connectable (#78687) 2022-09-18 13:13:08 -04:00
Franck Nijhof cce4496ad6 Remove mDNS iteration from Plugwise unique ID (#78680)
* Remove mDNS iteration from Plugwise unique ID

* Add iteration to tests
2022-09-18 13:10:11 -04:00
Raman Gupta ebeebeaec1 Handle multiple files properly in zwave_js update entity (#78658)
* Handle multiple files properly in zwave_js update entity

* Until we have progress, set in progress to true. And fix when we write state

* fix tests

* Assert we set in progress to true before we get progress

* Fix tests

* Comment
2022-09-18 13:10:10 -04:00
Franck Nijhof c8d16175da Update demetriek to 0.2.4 (#78646) 2022-09-18 13:10:09 -04:00
J. Nick Koston a2aa0e608d Add a helpful message to the config_entries.OperationNotAllowed exception (#78631)
We only expect this exception to be raised as a result of an
implementation problem. When it is raised during production
it is currently hard to trace down why its happening

See #75835
2022-09-18 13:10:09 -04:00
Sergio Conde Gómez 7eb98ffbd1 Bump qingping-ble to 0.7.0 (#78630) 2022-09-18 13:10:08 -04:00
J. Nick Koston 6e62080cd9 Fix reconnect race in HomeKit Controller (#78629) 2022-09-18 13:10:07 -04:00
J. Nick Koston 39dee6d426 Fix switchbot not accepting the first advertisement (#78610) 2022-09-18 13:10:07 -04:00
Franck Nijhof 3a89a49d4a Fix WebSocket condition testing (#78570) 2022-09-18 13:10:06 -04:00
On Freund ef66d8e705 Bump pyrisco to v0.5.5 (#78566)
Upgrade to pyrisco 0.5.5
2022-09-18 13:10:05 -04:00
Raman Gupta c1809681b6 Fix zwave_js update entity startup state (#78563)
* Fix update entity startup state

* Only write state if there is a change

* Add test to show that when an existing entity gets recreated, skipped version does not reset

* Remove unused blocks

* Update homeassistant/components/zwave_js/update.py

Co-authored-by: Franck Nijhof <frenck@frenck.nl>

Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2022-09-18 13:10:05 -04:00
J. Nick Koston 050c09df62 Bump aiohomekit to 1.5.8 (#78515) 2022-09-18 12:58:22 -04:00
J. Nick Koston e0b63ac488 Bump led-ble to 0.10.1 (#78511) 2022-09-18 12:58:21 -04:00
J. Nick Koston ed6575fefb Bump yalexs_ble to 1.9.2 (#78508) 2022-09-18 12:58:21 -04:00
J. Nick Koston 318ae7750a Bump PySwitchbot to 0.19.9 (#78504) 2022-09-18 12:58:20 -04:00
Teemu R 0525a1cd97 Bump python-songpal to 0.15.1 (#78481) 2022-09-18 12:58:20 -04:00
J. Nick Koston d31d4e2916 Bump bleak-retry-connector to 0.17.1 (#78474) 2022-09-18 12:58:19 -04:00
Erik Montnemery 40c5689507 Prevent deleting blueprints which are in use (#78444) 2022-09-18 12:57:40 -04:00
Raman Gupta a4749178f1 Only redact zwave_js values that are worth redacting (#78420)
* Only redact zwave_js values that are worth redacting

* Tweak test

* Use redacted fixture for test
2022-09-18 12:56:40 -04:00
Pete 8229e241f1 Fix fan speed regression for some xiaomi fans (#78406)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-09-18 12:56:39 -04:00
Franck Nijhof 2b40f3f1e5 2022.9.4 (#78438) 2022-09-14 11:01:27 +02:00
Franck Nijhof e839849456 Bumped version to 2022.9.4 2022-09-14 10:03:04 +02:00
Bram Kragten e711758cfd Update frontend to 20220907.2 (#78431) 2022-09-14 10:02:43 +02:00
Paulus Schoutsen 896955e4df 2022.9.3 (#78410) 2022-09-13 21:50:29 -04:00
Diogo Gomes 7b83807baa Retry on unavailable IPMA api (#78332)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-09-13 21:01:19 -04:00
Aaron Bach 6a197332c7 Fix bug with RainMachine update entity (#78411)
* Fix bug with RainMachine update entity

* Comment
2022-09-13 20:57:38 -04:00
Paulus Schoutsen 1955ff9e0d Bumped version to 2022.9.3 2022-09-13 16:31:51 -04:00
J. Nick Koston 29caf06439 Bump govee-ble to 0.17.3 (#78405) 2022-09-13 16:31:40 -04:00
Bram Kragten 0b5953038e Update frontend to 20220907.1 (#78404) 2022-09-13 16:31:39 -04:00
Marc Mueller f07e1bc500 Fix CI workflow caching (#78398) 2022-09-13 16:31:38 -04:00
J. Nick Koston 843d5f101a Fix flapping system log test (#78391)
Since we run tests with asyncio debug on, there is
a chance we will get an asyncio log message instead
of the one we want

Fixes https://github.com/home-assistant/core/actions/runs/3045080236/jobs/4906717578
2022-09-13 16:31:38 -04:00
Pascal Vizeli d98ed5c6f6 Unregister EcoWitt webhook at unload (#78388) 2022-09-13 16:31:37 -04:00
Erik Montnemery 8599472880 Don't allow partial update of timer settings (#78378) 2022-09-13 16:31:37 -04:00
Erik Montnemery 04d6bb085b Don't allow partial update of input_text settings (#78377) 2022-09-13 16:31:36 -04:00
Erik Montnemery 6f9a311cec Don't allow partial update of input_select settings (#78376) 2022-09-13 16:31:35 -04:00
Erik Montnemery 336179df6d Don't allow partial update of input_button settings (#78374) 2022-09-13 16:31:34 -04:00
Erik Montnemery 9459af30b0 Don't allow partial update of input_datetime settings (#78373) 2022-09-13 16:31:34 -04:00
Erik Montnemery ee07ca8caa Don't allow partial update of input_boolean settings (#78372) 2022-09-13 16:31:33 -04:00
Erik Montnemery 3beed13586 Don't allow partial update of counter settings (#78371) 2022-09-13 16:31:33 -04:00
J. Nick Koston f0753f7a97 Bump aiohomekit to 1.5.7 (#78369) 2022-09-13 16:31:32 -04:00
J. Nick Koston dd007cd765 Bump led-ble to 0.10.0 (#78367) 2022-09-13 16:31:31 -04:00
J. Nick Koston 7cdac3ee8c Bump xiaomi-ble to 0.10.0 (#78365) 2022-09-13 16:31:31 -04:00
J. Nick Koston cd7f65bb6a Bump xiaomi-ble to 0.9.3 (#78301) 2022-09-13 16:31:30 -04:00
J. Nick Koston b21a37cad5 Bump yalexs-ble to 1.9.0 (#78362) 2022-09-13 16:30:45 -04:00
J. Nick Koston bfcb9402ef Bump PySwitchbot to 0.19.8 (#78361)
* Bump PySwitchbot to 0.19.7

Changes for bleak 0.17

https://github.com/Danielhiversen/pySwitchbot/compare/0.19.6...0.19.7

* bump again to fix some more stale state bugs
2022-09-13 16:30:44 -04:00
Erik Montnemery ad396f0538 Don't allow partial update of input_number settings (#78356) 2022-09-13 16:30:44 -04:00
Erik Montnemery 12edfb3929 Drop initial when loading input_number from storage (#78354) 2022-09-13 16:30:43 -04:00
J. Nick Koston 47f6be77cc Bump bleak to 0.17.0 (#78333) 2022-09-13 16:30:42 -04:00
Erik Montnemery 9acf74d783 Fix calculating gas cost for gas measured in ft3 (#78327) 2022-09-13 16:30:42 -04:00
David F. Mulcahey 0aa2685e0c Fix sengled bulbs in ZHA (#78315)
* Fix sengled bulbs in ZHA

* fix tests

* update discovery data
2022-09-13 16:30:41 -04:00
J. Nick Koston a90b6d37bf Make yalexs_ble matcher more specific (#78307) 2022-09-13 16:30:40 -04:00
J. Nick Koston d6bf1a8caa Bump pySwitchbot to 0.19.6 (#78304)
No longer swallows exceptions from bleak connection errors which
was hiding the root cause of problems.

This was the original behavior from a long time ago which
does not make sense anymore since we retry a few times anyways

https://github.com/Danielhiversen/pySwitchbot/compare/0.19.5...0.19.6
2022-09-13 16:30:40 -04:00
J. Nick Koston 95a89448e0 Bump aiodiscover to 1.4.13 (#78253) 2022-09-13 16:30:39 -04:00
J. Nick Koston f6d26476b5 Bump bluetooth-auto-recovery to 0.3.3 (#78245)
Downgrades a few more loggers to debug since the only reason we check them is to give a better error message when the bluetooth adapter is blocked by rfkill.

https://github.com/Bluetooth-Devices/bluetooth-auto-recovery/compare/v0.3.2...v0.3.3

closes #78211
2022-09-13 16:30:38 -04:00
d-walsh 9640553b52 Fix missing dependency for dbus_next (#78235) 2022-09-13 16:30:38 -04:00
TheJulianJES 3129114d07 Bump PyViCare==2.17.0 (#78232) 2022-09-13 16:30:37 -04:00
Yevhenii Vaskivskyi 184a1c95f0 Bump blinkpy to 0.19.2 (#78097) 2022-09-13 16:30:36 -04:00
Robert Svensson f18ab504a5 Move up setup of service to make it more robust when running multiple instances of deCONZ (#77621) 2022-09-13 16:30:36 -04:00
Paulus Schoutsen 2bd71f62ea 2022.9.2 (#78169) 2022-09-11 13:28:28 -04:00
J. Nick Koston 296db8b2af Bump aiohomekit to 1.5.6 (#78228) 2022-09-11 12:21:29 -04:00
J. Nick Koston a277664187 Bump led-ble to 0.9.1 (#78226) 2022-09-11 12:21:28 -04:00
J. Nick Koston 1b7a06912a Bump yalexs-ble to 1.8.1 (#78225) 2022-09-11 12:21:27 -04:00
J. Nick Koston e7986a54a5 Bump PySwitchbot to 0.19.5 (#78224) 2022-09-11 12:21:26 -04:00
G Johansson de8b066a1d Bump pysensibo to 1.0.20 (#78222) 2022-09-11 12:21:25 -04:00
J. Nick Koston 4d4a87ba05 Bump led_ble to 0.8.5 (#78215)
* Bump led_ble to 0.8.4

Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.8.3...v0.8.4

* bump again
2022-09-11 12:21:24 -04:00
Aaron Bach 4b79e82e31 Bump regenmaschine to 2022.09.1 (#78210) 2022-09-11 12:21:24 -04:00
J. Nick Koston 1e8f461270 Bump bluetooth-adapters to 0.4.1 (#78205)
Switches to dbus-fast which fixes a file descriptor leak
2022-09-11 12:21:23 -04:00
Vincent Knoop Pathuis 6e88b8d3d5 Landis+Gyr integration: increase timeout and add debug logging (#78025) 2022-09-11 12:21:22 -04:00
J. Nick Koston a626ab4f1a Bump aiohomekit to 1.5.4 to handle stale ble connections at startup (#78203) 2022-09-10 14:37:06 -04:00
J. Nick Koston c7cb0d1a07 Close stale switchbot connections at setup time (#78202) 2022-09-10 14:37:05 -04:00
puddly 183c61b6ca Bump ZHA dependencies (#78201) 2022-09-10 14:37:05 -04:00
J. Nick Koston 95c20df367 Fix Yale Access Bluetooth not setting up when already connected at startup (#78199) 2022-09-10 14:37:04 -04:00
J. Nick Koston a969ce273a Fix switchbot not setting up when already connected at startup (#78198) 2022-09-10 14:37:03 -04:00
J. Nick Koston 5f90760176 Bump led-ble to 0.8.3 (#78188)
* Bump led-ble to 0.8.0

Fixes setup when the previous shutdown was not clean and
the device is still connected

* bump again

* bump again

* bump again
2022-09-10 14:37:03 -04:00
Pascal Vizeli 795be361b4 Add dependencies to ecowitt (#78187) 2022-09-10 14:37:02 -04:00
Rami Mosleh cdd5c809bb Fix sending notification to multiple targets in Pushover (#78111)
fix sending to mulitple targets
2022-09-10 14:37:01 -04:00
Paulus Schoutsen c731e2f125 Fix ecowitt typing (#78171) 2022-09-09 23:32:26 -04:00
J. Nick Koston 1789a8a385 Bump aiohomekit to 1.5.3 (#78170) 2022-09-09 23:20:55 -04:00
Paulus Schoutsen 57717f13fc Bumped version to 2022.9.2 2022-09-09 22:39:13 -04:00
J. Nick Koston e4aab6a818 Bump pySwitchbot to 0.19.1 (#78168) 2022-09-09 22:37:35 -04:00
Jc2k 258791626e Add missing moisture sensor to xiaomi_ble (#78160) 2022-09-09 22:37:35 -04:00
Pascal Vizeli 78802c8480 Bump aioecowitt to 2022.09.1 (#78142) 2022-09-09 22:37:34 -04:00
Yevhenii Vaskivskyi b24f3725d6 Add missing strings for errors in amberelectric config flow (#78140) 2022-09-09 22:37:33 -04:00
J. Nick Koston 06116f76fa Bump bluetooth-adapters to 0.3.6 (#78138) 2022-09-09 22:37:33 -04:00
Erik Montnemery 27c0a37053 Allow non-integers in threshold sensor config flow (#78137) 2022-09-09 22:37:32 -04:00
Erik Montnemery 2b961fd327 Improve unique_id collision checks in entity_platform (#78132) 2022-09-09 22:37:31 -04:00
Raman Gupta 125afb39f0 Fix zwave_js update entity (#78116)
* Test zwave_js update entity progress

* Block until firmware update is done

* Update homeassistant/components/zwave_js/update.py

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

* revert params

* unsub finished event listener

* fix tests

* Add test for returned failure

* refactor a little

* rename

* Remove unnecessary controller logic for mocking

* Clear event when resetting

* Comments

* readability

* Fix test

* Fix test

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-09-09 22:37:31 -04:00
Anders Melchiorsen 3ee62d619f Fix LIFX light turning on while fading off (#78095) 2022-09-09 22:37:30 -04:00
J. Nick Koston dc7c860c6a Fix switchbot writing state too frequently (#78094) 2022-09-09 22:37:29 -04:00
Paulus Schoutsen f042cc5d7b Handle missing supported brands (#78090) 2022-09-09 22:37:29 -04:00
Jan Bouwhuis 4c0872b4e4 Improve warning messages on invalid received modes (#77909) 2022-09-09 22:37:28 -04:00
Jan Bouwhuis 21f6b50f7c Clear MQTT discovery topic when a disabled entity is removed (#77757)
* Cleanup discovery on entity removal

* Add test

* Cleanup and test

* Test with clearing payload not unique id

* Address comments

* Tests cover and typing

* Just pass hass

* reuse code

* Follow up comments revert changes to cover tests

* Add test unique_id has priority over disabled

* Update homeassistant/components/mqtt/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-09-09 22:37:27 -04:00
Jan Bouwhuis d670df74cb Fix reload of MQTT config entries (#76089)
* Wait for unsubscribes

* Spelling comment

* Remove notify_all() during _register_mid()

* Update homeassistant/components/mqtt/client.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Correct handling reload manual set up MQTT items

* Save and restore device trigger subscriptions

* Clarify we are storing all remaining subscriptions

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2022-09-09 22:37:27 -04:00
Paulus Schoutsen 0a7f3f6ced 2022.9.1 (#78081) 2022-09-08 21:58:18 -04:00
rlippmann fee9a303ff Fix issue #77920 - ecobee remote sensors not updating (#78035) 2022-09-08 21:02:32 -04:00
Paulus Schoutsen a4f398a750 Bumped version to 2022.9.1 2022-09-08 16:50:47 -04:00
Jan Bouwhuis c873eae79c Allow OpenWeatherMap config flow to test using old API to pass (#78074)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2022-09-08 16:50:13 -04:00
Nathan Spencer d559b6482a Bump pylitterbot to 2022.9.1 (#78071) 2022-09-08 16:50:12 -04:00
Aaron Bach 760853f615 Fix bug with 1st gen RainMachine controllers and unknown API calls (#78070)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2022-09-08 16:50:12 -04:00
J. Nick Koston cfe8ebdad4 Bump bluetooth-auto-recovery to 0.3.2 (#78063) 2022-09-08 16:50:11 -04:00
J. Nick Koston 2ddd1b516c Bump bluetooth-adapters to 0.3.5 (#78052) 2022-09-08 16:50:10 -04:00
Martin Hjelmare 3b025b211e Fix zwave_js device re-interview (#78046)
* Handle stale node and entity info on re-interview

* Add test

* Unsubscribe on config entry unload
2022-09-08 16:50:10 -04:00
Maikel Punie 4009a32fb5 Bump velbus-aio to 2022.9.1 (#78039)
Bump velbusaio to 2022.9.1
2022-09-08 16:50:09 -04:00
Joakim Sørensen 6f3b49601e Extract lametric device from coordinator in notify (#78027) 2022-09-08 16:50:08 -04:00
Martin Hjelmare 31858ad779 Fix zwave_js default emulate hardware in options flow (#78024) 2022-09-08 16:50:08 -04:00
Raman Gupta ab9d9d599e Add value ID to zwave_js device diagnostics (#78015) 2022-09-08 16:50:07 -04:00
Yevhenii Vaskivskyi ce6d337bd5 Fix len method typo for Osram light (#78008)
* Fix `len` method typo for Osram light

* Fix typo in line 395
2022-09-08 16:50:06 -04:00
Raman Gupta 3fd887b1f2 Show progress for zwave_js.update entity (#77905)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-09-08 16:50:05 -04:00
Raman Gupta 996a3477b0 Increase rate limit for zwave_js updates
Al provided a new key which bumps the rate limit from 10k per hour to 100k per hour
2022-09-08 14:03:04 -04:00
Paulus Schoutsen 910f27f3a2 2022.9.0 (#77968) 2022-09-07 12:49:59 -04:00
Franck Nijhof 4ab5cdcb79 Bumped version to 2022.9.0 2022-09-07 17:46:53 +02:00
Bram Kragten e69fde6875 Update frontend to 20220907.0 (#77963) 2022-09-07 17:45:47 +02:00
J. Nick Koston 10f7e2ff8a Handle stale switchbot advertisement data while connected (#77956) 2022-09-07 17:45:42 +02:00
J. Nick Koston 3acc3af38c Bump PySwitchbot to 0.18.25 (#77935) 2022-09-07 17:45:36 +02:00
J. Nick Koston a3edbfc601 Small tweaks to improve performance of bluetooth matching (#77934)
* Small tweaks to improve performance of bluetooth matching

* Small tweaks to improve performance of bluetooth matching

* cleanup
2022-09-07 17:45:31 +02:00
J. Nick Koston 941a5e3820 Bump led-ble to 0.7.1 (#77931) 2022-09-07 17:45:26 +02:00
J. Nick Koston 2eeab820b7 Bump aiohomekit to 1.5.2 (#77927) 2022-09-07 17:45:21 +02:00
Franck Nijhof 8d0ebdd1f9 Revert "Add ability to ignore devices for UniFi Protect" (#77916) 2022-09-07 17:45:16 +02:00
Raman Gupta 9901b31316 Bump zwave-js-server-python to 0.41.1 (#77915)
* Bump zwave-js-server-python to 0.41.1

* Fix fixture
2022-09-07 17:45:11 +02:00
Chris McCurdy a4f528e908 Add additional method of retrieving UUID for LG soundbar configuration (#77856) 2022-09-07 17:45:05 +02:00
puddly 9aa87761cf Fix ZHA lighting initial hue/saturation attribute read (#77727)
* Handle the case of `current_hue` being `None`

* WIP unit tests
2022-09-07 17:45:00 +02:00
Matthew Simpson d1b637ea7a Bump btsmarthub_devicelist to 0.2.2 (#77609) 2022-09-07 17:44:54 +02:00
223 changed files with 9928 additions and 5371 deletions
+8 -5
View File
@@ -23,7 +23,8 @@ env:
CACHE_VERSION: 1
PIP_CACHE_VERSION: 1
HA_SHORT_VERSION: 2022.9
DEFAULT_PYTHON: 3.9
DEFAULT_PYTHON: 3.9.14
ALL_PYTHON_VERSIONS: "['3.9.14', '3.10.7']"
PRE_COMMIT_CACHE: ~/.cache/pre-commit
PIP_CACHE: /tmp/pip-cache
SQLALCHEMY_WARN_20: 1
@@ -46,6 +47,7 @@ jobs:
pre-commit_cache_key: ${{ steps.generate_pre-commit_cache_key.outputs.key }}
python_cache_key: ${{ steps.generate_python_cache_key.outputs.key }}
requirements: ${{ steps.core.outputs.requirements }}
python_versions: ${{ steps.info.outputs.python_versions }}
test_full_suite: ${{ steps.info.outputs.test_full_suite }}
test_group_count: ${{ steps.info.outputs.test_group_count }}
test_groups: ${{ steps.info.outputs.test_groups }}
@@ -143,6 +145,8 @@ jobs:
fi
# Output & sent to GitHub Actions
echo "python_versions: ${ALL_PYTHON_VERSIONS}"
echo "::set-output name=python_versions::${ALL_PYTHON_VERSIONS}"
echo "test_full_suite: ${test_full_suite}"
echo "::set-output name=test_full_suite::${test_full_suite}"
echo "integrations_glob: ${integrations_glob}"
@@ -169,7 +173,6 @@ jobs:
uses: actions/setup-python@v4.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache: "pip"
- name: Restore base Python virtual environment
id: cache-venv
uses: actions/cache@v3.0.8
@@ -464,7 +467,7 @@ jobs:
timeout-minutes: 60
strategy:
matrix:
python-version: ["3.9", "3.10"]
python-version: ${{ fromJSON(needs.info.outputs.python_versions) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v3.0.2
@@ -683,7 +686,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10"]
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
name: Run pip check ${{ matrix.python-version }}
steps:
- name: Check out code from GitHub
@@ -730,7 +733,7 @@ jobs:
fail-fast: false
matrix:
group: ${{ fromJson(needs.info.outputs.test_groups) }}
python-version: ["3.9", "3.10"]
python-version: ${{ fromJson(needs.info.outputs.python_versions) }}
name: >-
Run tests Python ${{ matrix.python-version }} (${{ matrix.group }})
steps:
+2 -2
View File
@@ -867,8 +867,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/qbittorrent/ @geoffreylagaisse
/homeassistant/components/qingping/ @bdraco
/tests/components/qingping/ @bdraco
/homeassistant/components/qingping/ @bdraco @skgsergio
/tests/components/qingping/ @bdraco @skgsergio
/homeassistant/components/qld_bushfire/ @exxamalte
/tests/components/qld_bushfire/ @exxamalte
/homeassistant/components/qnap_qsw/ @Noltari
@@ -15,6 +15,11 @@
},
"description": "Select the NMI of the site you would like to add"
}
},
"error": {
"invalid_api_token": "[%key:common::config_flow::error::invalid_api_key%]",
"no_site": "No site provided",
"unknown_error": "[%key:common::config_flow::error::unknown%]"
}
}
}
@@ -15,6 +15,11 @@
},
"description": "Go to {api_url} to generate an API key"
}
},
"error": {
"invalid_api_token": "Invalid API key",
"no_site": "No site provided",
"unknown_error": "Unexpected error"
}
}
}
@@ -9,6 +9,7 @@ import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components import blueprint
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@@ -20,6 +21,7 @@ from homeassistant.const import (
CONF_EVENT_DATA,
CONF_ID,
CONF_MODE,
CONF_PATH,
CONF_PLATFORM,
CONF_VARIABLES,
CONF_ZONE,
@@ -224,6 +226,21 @@ def areas_in_automation(hass: HomeAssistant, entity_id: str) -> list[str]:
return list(automation_entity.referenced_areas)
@callback
def automations_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
"""Return all automations that reference the blueprint."""
if DOMAIN not in hass.data:
return []
component = hass.data[DOMAIN]
return [
automation_entity.entity_id
for automation_entity in component.entities
if automation_entity.referenced_blueprint == blueprint_path
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up all automations."""
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
@@ -346,7 +363,14 @@ class AutomationEntity(ToggleEntity, RestoreEntity):
return self.action_script.referenced_areas
@property
def referenced_devices(self):
def referenced_blueprint(self) -> str | None:
"""Return referenced blueprint or None."""
if self._blueprint_inputs is None:
return None
return cast(str, self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH])
@property
def referenced_devices(self) -> set[str]:
"""Return a set of referenced devices."""
if self._referenced_devices is not None:
return self._referenced_devices
@@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER
DATA_BLUEPRINTS = "automation_blueprints"
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
"""Return True if any automation references the blueprint."""
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
return len(automations_with_blueprint(hass, blueprint_path)) > 0
@singleton(DATA_BLUEPRINTS)
@callback
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
"""Get automation blueprints."""
return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER)
return blueprint.DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use)
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "blink",
"name": "Blink",
"documentation": "https://www.home-assistant.io/integrations/blink",
"requirements": ["blinkpy==0.19.0"],
"requirements": ["blinkpy==0.19.2"],
"codeowners": ["@fronzbot"],
"dhcp": [
{
@@ -3,7 +3,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType
from . import websocket_api
from .const import DOMAIN # noqa: F401
from .const import CONF_USE_BLUEPRINT, DOMAIN # noqa: F401
from .errors import ( # noqa: F401
BlueprintException,
BlueprintWithNameException,
@@ -91,3 +91,11 @@ class FileAlreadyExists(BlueprintWithNameException):
def __init__(self, domain: str, blueprint_name: str) -> None:
"""Initialize blueprint exception."""
super().__init__(domain, blueprint_name, "Blueprint already exists")
class BlueprintInUse(BlueprintWithNameException):
"""Error when a blueprint is in use."""
def __init__(self, domain: str, blueprint_name: str) -> None:
"""Initialize blueprint exception."""
super().__init__(domain, blueprint_name, "Blueprint in use")
@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable
import logging
import pathlib
import shutil
@@ -35,6 +36,7 @@ from .const import (
)
from .errors import (
BlueprintException,
BlueprintInUse,
FailedToLoad,
FileAlreadyExists,
InvalidBlueprint,
@@ -183,11 +185,13 @@ class DomainBlueprints:
hass: HomeAssistant,
domain: str,
logger: logging.Logger,
blueprint_in_use: Callable[[HomeAssistant, str], bool],
) -> None:
"""Initialize a domain blueprints instance."""
self.hass = hass
self.domain = domain
self.logger = logger
self._blueprint_in_use = blueprint_in_use
self._blueprints: dict[str, Blueprint | None] = {}
self._load_lock = asyncio.Lock()
@@ -302,6 +306,8 @@ class DomainBlueprints:
async def async_remove_blueprint(self, blueprint_path: str) -> None:
"""Remove a blueprint file."""
if self._blueprint_in_use(self.hass, blueprint_path):
raise BlueprintInUse(self.domain, blueprint_path)
path = self.blueprint_folder / blueprint_path
await self.hass.async_add_executor_job(path.unlink)
self._blueprints[blueprint_path] = None
+1 -1
View File
@@ -58,7 +58,7 @@ class AdapterDetails(TypedDict, total=False):
address: str
sw_version: str
hw_version: str
hw_version: str | None
passive_scan: bool
@@ -60,6 +60,7 @@ APPLE_DEVICE_ID_START_BYTE: Final = 0x10 # bluetooth_le_tracker
APPLE_START_BYTES_WANTED: Final = {APPLE_DEVICE_ID_START_BYTE, APPLE_HOMEKIT_START_BYTE}
RSSI_SWITCH_THRESHOLD = 6
NO_RSSI_VALUE = -1000
_LOGGER = logging.getLogger(__name__)
@@ -83,7 +84,7 @@ def _prefer_previous_adv(
STALE_ADVERTISEMENT_SECONDS,
)
return False
if new.device.rssi - RSSI_SWITCH_THRESHOLD > old.device.rssi:
if new.device.rssi - RSSI_SWITCH_THRESHOLD > (old.device.rssi or NO_RSSI_VALUE):
# If new advertisement is RSSI_SWITCH_THRESHOLD more, the new one is preferred
if new.source != old.source:
_LOGGER.debug(
@@ -384,11 +385,11 @@ class BluetoothManager:
callback_matcher[CONNECTABLE] = matcher.get(CONNECTABLE, True)
connectable = callback_matcher[CONNECTABLE]
self._callback_index.add_with_address(callback_matcher)
self._callback_index.add_callback_matcher(callback_matcher)
@hass_callback
def _async_remove_callback() -> None:
self._callback_index.remove_with_address(callback_matcher)
self._callback_index.remove_callback_matcher(callback_matcher)
# If we have history for the subscriber, we can trigger the callback
# immediately with the last packet so the subscriber can see the
@@ -5,9 +5,11 @@
"dependencies": ["usb"],
"quality_scale": "internal",
"requirements": [
"bleak==0.16.0",
"bluetooth-adapters==0.3.4",
"bluetooth-auto-recovery==0.3.1"
"bleak==0.17.0",
"bleak-retry-connector==1.17.1",
"bluetooth-adapters==0.4.1",
"bluetooth-auto-recovery==0.3.3",
"dbus-fast==1.5.1"
],
"codeowners": ["@bdraco"],
"config_flow": true,
+72 -49
View File
@@ -173,36 +173,40 @@ class BluetoothMatcherIndexBase(Generic[_T]):
self.service_data_uuid_set: set[str] = set()
self.manufacturer_id_set: set[int] = set()
def add(self, matcher: _T) -> None:
def add(self, matcher: _T) -> bool:
"""Add a matcher to the index.
Matchers must end up only in one bucket.
We put them in the bucket that they are most likely to match.
"""
# Local name is the cheapest to match since its just a dict lookup
if LOCAL_NAME in matcher:
self.local_name.setdefault(
_local_name_to_index_key(matcher[LOCAL_NAME]), []
).append(matcher)
return
return True
# Manufacturer data is 2nd cheapest since its all ints
if MANUFACTURER_ID in matcher:
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
matcher
)
return True
if SERVICE_UUID in matcher:
self.service_uuid.setdefault(matcher[SERVICE_UUID], []).append(matcher)
return
return True
if SERVICE_DATA_UUID in matcher:
self.service_data_uuid.setdefault(matcher[SERVICE_DATA_UUID], []).append(
matcher
)
return
return True
if MANUFACTURER_ID in matcher:
self.manufacturer_id.setdefault(matcher[MANUFACTURER_ID], []).append(
matcher
)
return
return False
def remove(self, matcher: _T) -> None:
def remove(self, matcher: _T) -> bool:
"""Remove a matcher from the index.
Matchers only end up in one bucket, so once we have
@@ -212,19 +216,21 @@ class BluetoothMatcherIndexBase(Generic[_T]):
self.local_name[_local_name_to_index_key(matcher[LOCAL_NAME])].remove(
matcher
)
return
if SERVICE_UUID in matcher:
self.service_uuid[matcher[SERVICE_UUID]].remove(matcher)
return
if SERVICE_DATA_UUID in matcher:
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher)
return
return True
if MANUFACTURER_ID in matcher:
self.manufacturer_id[matcher[MANUFACTURER_ID]].remove(matcher)
return
return True
if SERVICE_UUID in matcher:
self.service_uuid[matcher[SERVICE_UUID]].remove(matcher)
return True
if SERVICE_DATA_UUID in matcher:
self.service_data_uuid[matcher[SERVICE_DATA_UUID]].remove(matcher)
return True
return False
def build(self) -> None:
"""Rebuild the index sets."""
@@ -235,33 +241,36 @@ class BluetoothMatcherIndexBase(Generic[_T]):
def match(self, service_info: BluetoothServiceInfoBleak) -> list[_T]:
"""Check for a match."""
matches = []
if len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
if service_info.name and len(service_info.name) >= LOCAL_NAME_MIN_MATCH_LENGTH:
for matcher in self.local_name.get(
service_info.name[:LOCAL_NAME_MIN_MATCH_LENGTH], []
):
if ble_device_matches(matcher, service_info):
matches.append(matcher)
for service_data_uuid in self.service_data_uuid_set.intersection(
service_info.service_data
):
for matcher in self.service_data_uuid[service_data_uuid]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
if self.service_data_uuid_set and service_info.service_data:
for service_data_uuid in self.service_data_uuid_set.intersection(
service_info.service_data
):
for matcher in self.service_data_uuid[service_data_uuid]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
for manufacturer_id in self.manufacturer_id_set.intersection(
service_info.manufacturer_data
):
for matcher in self.manufacturer_id[manufacturer_id]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
if self.manufacturer_id_set and service_info.manufacturer_data:
for manufacturer_id in self.manufacturer_id_set.intersection(
service_info.manufacturer_data
):
for matcher in self.manufacturer_id[manufacturer_id]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
for service_uuid in self.service_uuid_set.intersection(
service_info.service_uuids
):
for matcher in self.service_uuid[service_uuid]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
if self.service_uuid_set and service_info.service_uuids:
for service_uuid in self.service_uuid_set.intersection(
service_info.service_uuids
):
for matcher in self.service_uuid[service_uuid]:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
return matches
@@ -279,8 +288,11 @@ class BluetoothCallbackMatcherIndex(
"""Initialize the matcher index."""
super().__init__()
self.address: dict[str, list[BluetoothCallbackMatcherWithCallback]] = {}
self.connectable: list[BluetoothCallbackMatcherWithCallback] = []
def add_with_address(self, matcher: BluetoothCallbackMatcherWithCallback) -> None:
def add_callback_matcher(
self, matcher: BluetoothCallbackMatcherWithCallback
) -> None:
"""Add a matcher to the index.
Matchers must end up only in one bucket.
@@ -291,10 +303,15 @@ class BluetoothCallbackMatcherIndex(
self.address.setdefault(matcher[ADDRESS], []).append(matcher)
return
super().add(matcher)
self.build()
if super().add(matcher):
self.build()
return
def remove_with_address(
if CONNECTABLE in matcher:
self.connectable.append(matcher)
return
def remove_callback_matcher(
self, matcher: BluetoothCallbackMatcherWithCallback
) -> None:
"""Remove a matcher from the index.
@@ -306,8 +323,13 @@ class BluetoothCallbackMatcherIndex(
self.address[matcher[ADDRESS]].remove(matcher)
return
super().remove(matcher)
self.build()
if super().remove(matcher):
self.build()
return
if CONNECTABLE in matcher:
self.connectable.remove(matcher)
return
def match_callbacks(
self, service_info: BluetoothServiceInfoBleak
@@ -317,6 +339,9 @@ class BluetoothCallbackMatcherIndex(
for matcher in self.address.get(service_info.address, []):
if ble_device_matches(matcher, service_info):
matches.append(matcher)
for matcher in self.connectable:
if ble_device_matches(matcher, service_info):
matches.append(matcher)
return matches
@@ -347,12 +372,9 @@ def ble_device_matches(
service_info: BluetoothServiceInfoBleak,
) -> bool:
"""Check if a ble device and advertisement_data matches the matcher."""
device = service_info.device
# Don't check address here since all callers already
# check the address and we don't want to double check
# since it would result in an unreachable reject case.
if matcher.get(CONNECTABLE, True) and not service_info.connectable:
return False
@@ -379,7 +401,8 @@ def ble_device_matches(
return False
if (local_name := matcher.get(LOCAL_NAME)) and (
(device_name := advertisement_data.local_name or device.name) is None
(device_name := advertisement_data.local_name or service_info.device.name)
is None
or not _memorized_fnmatch(
device_name,
local_name,
@@ -17,7 +17,7 @@ from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
from bleak.backends.device import BLEDevice
from bleak.backends.scanner import AdvertisementData
from dbus_next import InvalidMessageError
from dbus_fast import InvalidMessageError
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import (
+1 -1
View File
@@ -46,7 +46,7 @@ async def async_get_bluetooth_adapters() -> dict[str, AdapterDetails]:
adapters[adapter] = AdapterDetails(
address=adapter1["Address"],
sw_version=adapter1["Name"], # This is actually the BlueZ version
hw_version=adapter1["Modalias"],
hw_version=adapter1.get("Modalias"),
passive_scan="org.bluez.AdvertisementMonitorManager1" in details,
)
return adapters
@@ -2,7 +2,7 @@
"domain": "bmw_connected_drive",
"name": "BMW Connected Drive",
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"requirements": ["bimmer_connected==0.10.2"],
"requirements": ["bimmer_connected==0.10.4"],
"codeowners": ["@gerard33", "@rikroe"],
"config_flow": true,
"iot_class": "cloud_polling",
+5 -1
View File
@@ -1,6 +1,7 @@
"""Config flow for Bond integration."""
from __future__ import annotations
import asyncio
import contextlib
from http import HTTPStatus
import logging
@@ -83,7 +84,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
instead ask them to manually enter the token.
"""
host = self._discovered[CONF_HOST]
if not (token := await async_get_token(self.hass, host)):
try:
if not (token := await async_get_token(self.hass, host)):
return
except asyncio.TimeoutError:
return
self._discovered[CONF_ACCESS_TOKEN] = token
@@ -2,7 +2,7 @@
"domain": "bt_smarthub",
"name": "BT Smart Hub",
"documentation": "https://www.home-assistant.io/integrations/bt_smarthub",
"requirements": ["btsmarthub_devicelist==0.2.0"],
"requirements": ["btsmarthub_devicelist==0.2.2"],
"codeowners": ["@jxwolstenholme"],
"iot_class": "local_polling",
"loggers": ["btsmarthub_devicelist"]
+6 -17
View File
@@ -47,7 +47,7 @@ SERVICE_CONFIGURE = "configure"
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL, default=DEFAULT_INITIAL): cv.positive_int,
vol.Required(CONF_NAME): vol.All(cv.string, vol.Length(min=1)),
@@ -57,16 +57,6 @@ CREATE_FIELDS = {
vol.Optional(CONF_STEP, default=DEFAULT_STEP): cv.positive_int,
}
UPDATE_FIELDS = {
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL): cv.positive_int,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MAXIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_MINIMUM): vol.Any(None, vol.Coerce(int)),
vol.Optional(CONF_RESTORE): cv.boolean,
vol.Optional(CONF_STEP): cv.positive_int,
}
def _none_to_empty_dict(value):
if value is None:
@@ -128,7 +118,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
component.async_register_entity_service(SERVICE_INCREMENT, {}, "async_increment")
@@ -152,12 +142,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class CounterStorageCollection(collection.StorageCollection):
"""Input storage based collection."""
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
@@ -166,8 +155,8 @@ class CounterStorageCollection(collection.StorageCollection):
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
class Counter(RestoreEntity):
+3 -3
View File
@@ -43,6 +43,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err
if not hass.data[DOMAIN]:
async_setup_services(hass)
gateway = hass.data[DOMAIN][config_entry.entry_id] = DeconzGateway(
hass, config_entry, api
)
@@ -53,9 +56,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await async_setup_events(gateway)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
if len(hass.data[DOMAIN]) == 1:
async_setup_services(hass)
api.start()
config_entry.async_on_unload(
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "dhcp",
"name": "DHCP Discovery",
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"requirements": ["scapy==2.4.5", "aiodiscover==1.4.11"],
"requirements": ["scapy==2.4.5", "aiodiscover==1.4.13"],
"codeowners": ["@bdraco"],
"quality_scale": "internal",
"iot_class": "local_push",
+10 -5
View File
@@ -29,7 +29,7 @@ from .const import DOMAIN, ECOBEE_MODEL_TO_NAME, MANUFACTURER
class EcobeeSensorEntityDescriptionMixin:
"""Represent the required ecobee entity description attributes."""
runtime_key: str
runtime_key: str | None
@dataclass
@@ -46,7 +46,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=TEMP_FAHRENHEIT,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualTemperature",
runtime_key=None,
),
EcobeeSensorEntityDescription(
key="humidity",
@@ -54,7 +54,7 @@ SENSOR_TYPES: tuple[EcobeeSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
runtime_key="actualHumidity",
runtime_key=None,
),
EcobeeSensorEntityDescription(
key="co2PPM",
@@ -194,6 +194,11 @@ class EcobeeSensor(SensorEntity):
for item in sensor["capability"]:
if item["type"] != self.entity_description.key:
continue
thermostat = self.data.ecobee.get_thermostat(self.index)
self._state = thermostat["runtime"][self.entity_description.runtime_key]
if self.entity_description.runtime_key is None:
self._state = item["value"]
else:
thermostat = self.data.ecobee.get_thermostat(self.index)
self._state = thermostat["runtime"][
self.entity_description.runtime_key
]
break
@@ -44,6 +44,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
@@ -68,4 +68,4 @@ class EcowittBinarySensorEntity(EcowittEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self.ecowitt.value > 0
return bool(self.ecowitt.value)
@@ -25,13 +25,13 @@ async def async_get_device_diagnostics(
"device": {
"name": station.station,
"model": station.model,
"frequency": station.frequency,
"frequency": station.frequence,
"version": station.version,
},
"raw": ecowitt.last_values[station_id],
"sensors": {
sensor.key: sensor.value
for sensor in station.sensors
for sensor in ecowitt.sensors.values()
if sensor.station.key == station_id
},
}
@@ -3,7 +3,8 @@
"name": "Ecowitt",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ecowitt",
"requirements": ["aioecowitt==2022.08.3"],
"dependencies": ["webhook"],
"requirements": ["aioecowitt==2022.09.1"],
"codeowners": ["@pvizeli"],
"iot_class": "local_push"
}
+4 -1
View File
@@ -1,5 +1,8 @@
"""Support for Ecowitt Weather Stations."""
from __future__ import annotations
import dataclasses
from datetime import datetime
from typing import Final
from aioecowitt import EcoWittListener, EcoWittSensor, EcoWittSensorTypes
@@ -242,6 +245,6 @@ class EcowittSensorEntity(EcowittEntity, SensorEntity):
self.entity_description = description
@property
def native_value(self) -> StateType:
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.ecowitt.value
+2 -1
View File
@@ -20,6 +20,7 @@ from homeassistant.const import (
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
ENERGY_WATT_HOUR,
VOLUME_CUBIC_FEET,
VOLUME_CUBIC_METERS,
)
from homeassistant.core import (
@@ -44,7 +45,7 @@ SUPPORTED_STATE_CLASSES = [
SensorStateClass.TOTAL_INCREASING,
]
VALID_ENERGY_UNITS = [ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR, ENERGY_MEGA_WATT_HOUR]
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
VALID_ENERGY_UNITS_GAS = [VOLUME_CUBIC_FEET, VOLUME_CUBIC_METERS] + VALID_ENERGY_UNITS
_LOGGER = logging.getLogger(__name__)
@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20220906.0"],
"requirements": ["home-assistant-frontend==20220907.2"],
"dependencies": [
"api",
"auth",
@@ -260,8 +260,6 @@ class BrightnessTrait(_Trait):
brightness = self.state.attributes.get(light.ATTR_BRIGHTNESS)
if brightness is not None:
response["brightness"] = round(100 * (brightness / 255))
else:
response["brightness"] = 0
return response
@@ -17,6 +17,11 @@
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
"connectable": false
},
{
"manufacturer_id": 57391,
"service_uuid": "00008351-0000-1000-8000-00805f9b34fb",
"connectable": false
},
{
"manufacturer_id": 18994,
"service_uuid": "00008551-0000-1000-8000-00805f9b34fb",
@@ -53,7 +58,7 @@
"connectable": false
}
],
"requirements": ["govee-ble==0.17.2"],
"requirements": ["govee-ble==0.17.3"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"
+8 -2
View File
@@ -92,7 +92,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the valve off (closed)."""
"""Turn the switch off."""
if not self._attr_is_on:
return
try:
async with self._client:
await self._client.valve.close()
@@ -103,7 +106,10 @@ class ValveControllerSwitch(ValveControllerEntity, SwitchEntity):
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the valve on (open)."""
"""Turn the switch on."""
if self._attr_is_on:
return
try:
async with self._client:
await self._client.valve.open()
@@ -423,7 +423,7 @@ class HKDevice:
if self._polling_interval_remover:
self._polling_interval_remover()
await self.pairing.close()
await self.pairing.shutdown()
await self.hass.config_entries.async_unload_platforms(
self.config_entry, self.platforms
@@ -3,7 +3,7 @@
"name": "HomeKit Controller",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"requirements": ["aiohomekit==1.5.1"],
"requirements": ["aiohomekit==1.5.12"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."],
"bluetooth": [{ "manufacturer_id": 76, "manufacturer_data_start": [6] }],
"dependencies": ["bluetooth", "zeroconf"],
+1 -1
View File
@@ -2,7 +2,7 @@
"domain": "imap",
"name": "IMAP",
"documentation": "https://www.home-assistant.io/integrations/imap",
"requirements": ["aioimaplib==1.0.0"],
"requirements": ["aioimaplib==1.0.1"],
"codeowners": [],
"iot_class": "cloud_push",
"loggers": ["aioimaplib"]
@@ -37,20 +37,25 @@ _LOGGER = logging.getLogger(__name__)
CONF_INITIAL = "initial"
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_INITIAL): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INITIAL): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
}
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))},
{
DOMAIN: cv.schema_with_slug_keys(
vol.Any(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_INITIAL): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
},
None,
)
)
},
extra=vol.ALLOW_EXTRA,
)
@@ -62,12 +67,11 @@ STORAGE_VERSION = 1
class InputBooleanStorageCollection(collection.StorageCollection):
"""Input boolean collection stored in storage."""
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
@@ -76,8 +80,8 @@ class InputBooleanStorageCollection(collection.StorageCollection):
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
@bind_hass
@@ -118,7 +122,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -30,18 +30,23 @@ DOMAIN = "input_button"
_LOGGER = logging.getLogger(__name__)
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_ICON): cv.icon,
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}
CONFIG_SCHEMA = vol.Schema(
{DOMAIN: cv.schema_with_slug_keys(vol.Any(UPDATE_FIELDS, None))},
{
DOMAIN: cv.schema_with_slug_keys(
vol.Any(
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.icon,
},
None,
)
)
},
extra=vol.ALLOW_EXTRA,
)
@@ -53,12 +58,11 @@ STORAGE_VERSION = 1
class InputButtonStorageCollection(collection.StorageCollection):
"""Input button collection stored in storage."""
CREATE_SCHEMA = vol.Schema(CREATE_FIELDS)
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(STORAGE_FIELDS)
async def _process_create_data(self, data: dict) -> vol.Schema:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
@@ -67,8 +71,8 @@ class InputButtonStorageCollection(collection.StorageCollection):
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return {**data, **update_data}
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -103,7 +107,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -61,20 +61,13 @@ def validate_set_datetime_attrs(config):
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_HAS_DATE, default=False): cv.boolean,
vol.Optional(CONF_HAS_TIME, default=False): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL): cv.string,
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_HAS_DATE): cv.boolean,
vol.Optional(CONF_HAS_TIME): cv.boolean,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_INITIAL): cv.string,
}
def has_date_or_time(conf):
@@ -167,7 +160,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -213,12 +206,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class DateTimeStorageCollection(collection.StorageCollection):
"""Input storage based collection."""
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, has_date_or_time))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, has_date_or_time))
async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
@@ -227,8 +219,8 @@ class DateTimeStorageCollection(collection.StorageCollection):
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return has_date_or_time({**data, **update_data})
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
class InputDatetime(RestoreEntity):
@@ -65,7 +65,7 @@ def _cv_input_number(cfg):
return cfg
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Required(CONF_MIN): vol.Coerce(float),
vol.Required(CONF_MAX): vol.Coerce(float),
@@ -76,17 +76,6 @@ CREATE_FIELDS = {
vol.Optional(CONF_MODE, default=MODE_SLIDER): vol.In([MODE_BOX, MODE_SLIDER]),
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN): vol.Coerce(float),
vol.Optional(CONF_MAX): vol.Coerce(float),
vol.Optional(CONF_INITIAL): vol.Coerce(float),
vol.Optional(CONF_STEP): vol.All(vol.Coerce(float), vol.Range(min=1e-9)),
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_MODE): vol.In([MODE_BOX, MODE_SLIDER]),
}
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: cv.schema_with_slug_keys(
@@ -148,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -184,22 +173,37 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class NumberStorageCollection(collection.StorageCollection):
"""Input storage based collection."""
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_number))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_number))
async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
"""Suggest an ID based on the config."""
return info[CONF_NAME]
async def _async_load_data(self) -> dict | None:
"""Load the data.
A past bug caused frontend to add initial value to all input numbers.
This drops that.
"""
data = await super()._async_load_data()
if data is None:
return data
for number in data["items"]:
number.pop(CONF_INITIAL, None)
return data
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_number({**data, **update_data})
update_data = self.SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
class InputNumber(RestoreEntity):
@@ -56,7 +56,7 @@ def _unique(options: Any) -> Any:
raise HomeAssistantError("Duplicate options are not allowed") from exc
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Required(CONF_OPTIONS): vol.All(
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
@@ -64,14 +64,6 @@ CREATE_FIELDS = {
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_OPTIONS): vol.All(
cv.ensure_list, vol.Length(min=1), _unique, [cv.string]
),
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
}
def _remove_duplicates(options: list[str], name: str | None) -> list[str]:
@@ -172,7 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -238,12 +230,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class InputSelectStorageCollection(collection.StorageCollection):
"""Input storage based collection."""
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_select))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_select))
async def _process_create_data(self, data: dict[str, Any]) -> dict[str, Any]:
"""Validate the config is valid."""
return cast(dict[str, Any], self.CREATE_SCHEMA(data))
return cast(dict[str, Any], self.CREATE_UPDATE_SCHEMA(data))
@callback
def _get_suggested_id(self, info: dict[str, Any]) -> str:
@@ -254,8 +245,8 @@ class InputSelectStorageCollection(collection.StorageCollection):
self, data: dict[str, Any], update_data: dict[str, Any]
) -> dict[str, Any]:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_select({**data, **update_data})
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
class InputSelect(SelectEntity, RestoreEntity):
@@ -51,7 +51,7 @@ SERVICE_SET_VALUE = "set_value"
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1
CREATE_FIELDS = {
STORAGE_FIELDS = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
@@ -61,16 +61,6 @@ CREATE_FIELDS = {
vol.Optional(CONF_PATTERN): cv.string,
vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In([MODE_TEXT, MODE_PASSWORD]),
}
UPDATE_FIELDS = {
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN): vol.Coerce(int),
vol.Optional(CONF_MAX): vol.Coerce(int),
vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_PATTERN): cv.string,
vol.Optional(CONF_MODE): vol.In([MODE_TEXT, MODE_PASSWORD]),
}
def _cv_input_text(cfg):
@@ -147,7 +137,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
await storage_collection.async_load()
collection.StorageCollectionWebsocket(
storage_collection, DOMAIN, DOMAIN, CREATE_FIELDS, UPDATE_FIELDS
storage_collection, DOMAIN, DOMAIN, STORAGE_FIELDS, STORAGE_FIELDS
).async_setup(hass)
async def reload_service_handler(service_call: ServiceCall) -> None:
@@ -177,12 +167,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
class InputTextStorageCollection(collection.StorageCollection):
"""Input storage based collection."""
CREATE_SCHEMA = vol.Schema(vol.All(CREATE_FIELDS, _cv_input_text))
UPDATE_SCHEMA = vol.Schema(UPDATE_FIELDS)
CREATE_UPDATE_SCHEMA = vol.Schema(vol.All(STORAGE_FIELDS, _cv_input_text))
async def _process_create_data(self, data: dict) -> dict:
"""Validate the config is valid."""
return self.CREATE_SCHEMA(data)
return self.CREATE_UPDATE_SCHEMA(data)
@callback
def _get_suggested_id(self, info: dict) -> str:
@@ -191,8 +180,8 @@ class InputTextStorageCollection(collection.StorageCollection):
async def _update_data(self, data: dict, update_data: dict) -> dict:
"""Return a new updated data object."""
update_data = self.UPDATE_SCHEMA(update_data)
return _cv_input_text({**data, **update_data})
update_data = self.CREATE_UPDATE_SCHEMA(update_data)
return {CONF_ID: data[CONF_ID]} | update_data
class InputText(RestoreEntity):
+46 -4
View File
@@ -1,19 +1,61 @@
"""Component for the Portuguese weather service - IPMA."""
import logging
import async_timeout
from pyipma import IPMAException
from pyipma.api import IPMA_API
from pyipma.location import Location
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .config_flow import IpmaFlowHandler # noqa: F401
from .const import DOMAIN # noqa: F401
from .const import DATA_API, DATA_LOCATION, DOMAIN
DEFAULT_NAME = "ipma"
PLATFORMS = [Platform.WEATHER]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_get_api(hass):
"""Get the pyipma api object."""
websession = async_get_clientsession(hass)
return IPMA_API(websession)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Set up IPMA station as config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE]
api = await async_get_api(hass)
try:
async with async_timeout.timeout(30):
location = await Location.get(api, float(latitude), float(longitude))
_LOGGER.debug(
"Initializing for coordinates %s, %s -> station %s (%d, %d)",
latitude,
longitude,
location.station,
location.id_station,
location.global_id_local,
)
except IPMAException as err:
raise ConfigEntryNotReady(
f"Could not get location for ({latitude},{longitude})"
) from err
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = {DATA_API: api, DATA_LOCATION: location}
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
+3
View File
@@ -6,3 +6,6 @@ DOMAIN = "ipma"
HOME_LOCATION_NAME = "Home"
ENTITY_ID_SENSOR_FORMAT_HOME = f"{WEATHER_DOMAIN}.ipma_{HOME_LOCATION_NAME}"
DATA_LOCATION = "location"
DATA_API = "api"
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Instituto Portugu\u00eas do Mar e Atmosfera (IPMA)",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ipma",
"requirements": ["pyipma==3.0.2"],
"requirements": ["pyipma==3.0.4"],
"codeowners": ["@dgomes", "@abmantis"],
"iot_class": "cloud_polling",
"loggers": ["geopy", "pyipma"]
+4 -29
View File
@@ -48,11 +48,12 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_registry
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.sun import is_up
from homeassistant.util import Throttle
from .const import DATA_API, DATA_LOCATION, DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Instituto Português do Mar e Atmosfera"
@@ -95,13 +96,10 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add a weather entity from a config_entry."""
latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE]
api = hass.data[DOMAIN][config_entry.entry_id][DATA_API]
location = hass.data[DOMAIN][config_entry.entry_id][DATA_LOCATION]
mode = config_entry.data[CONF_MODE]
api = await async_get_api(hass)
location = await async_get_location(hass, api, latitude, longitude)
# Migrate old unique_id
@callback
def _async_migrator(entity_entry: entity_registry.RegistryEntry):
@@ -127,29 +125,6 @@ async def async_setup_entry(
async_add_entities([IPMAWeather(location, api, config_entry.data)], True)
async def async_get_api(hass):
"""Get the pyipma api object."""
websession = async_get_clientsession(hass)
return IPMA_API(websession)
async def async_get_location(hass, api, latitude, longitude):
"""Retrieve pyipma location, location name to be used as the entity name."""
async with async_timeout.timeout(30):
location = await Location.get(api, float(latitude), float(longitude))
_LOGGER.debug(
"Initializing for coordinates %s, %s -> station %s (%d, %d)",
latitude,
longitude,
location.station,
location.id_station,
location.global_id_local,
)
return location
class IPMAWeather(WeatherEntity):
"""Representation of a weather condition."""
@@ -2,7 +2,7 @@
"domain": "lametric",
"name": "LaMetric",
"documentation": "https://www.home-assistant.io/integrations/lametric",
"requirements": ["demetriek==0.2.2"],
"requirements": ["demetriek==0.2.4"],
"codeowners": ["@robbiet480", "@frenck"],
"iot_class": "local_polling",
"dependencies": ["application_credentials"],
+5 -2
View File
@@ -21,6 +21,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND, DOMAIN
from .coordinator import LaMetricDataUpdateCoordinator
async def async_get_service(
@@ -31,8 +32,10 @@ async def async_get_service(
"""Get the LaMetric notification service."""
if discovery_info is None:
return None
lametric: LaMetricDevice = hass.data[DOMAIN][discovery_info["entry_id"]]
return LaMetricNotificationService(lametric)
coordinator: LaMetricDataUpdateCoordinator = hass.data[DOMAIN][
discovery_info["entry_id"]
]
return LaMetricNotificationService(coordinator.lametric)
class LaMetricNotificationService(BaseNotificationService):
@@ -31,9 +31,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Polling on %s", entry.data[CONF_DEVICE])
return await hass.async_add_executor_job(api.read)
# No automatic polling and no initial refresh of data is being done at this point,
# to prevent battery drain. The user will have to do it manually.
# Polling is only daily to prevent battery drain.
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
@@ -14,7 +14,7 @@ from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE
from homeassistant.exceptions import HomeAssistantError
from .const import DOMAIN
from .const import DOMAIN, ULTRAHEAT_TIMEOUT
_LOGGER = logging.getLogger(__name__)
@@ -43,6 +43,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
dev_path = await self.hass.async_add_executor_job(
get_serial_by_id, user_input[CONF_DEVICE]
)
_LOGGER.debug("Using this path : %s", dev_path)
try:
return await self.validate_and_create_entry(dev_path)
@@ -76,6 +77,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Try to connect to the device path and return an entry."""
model, device_number = await self.validate_ultraheat(dev_path)
_LOGGER.debug("Got model %s and device_number %s", model, device_number)
await self.async_set_unique_id(device_number)
self._abort_if_unique_id_configured()
data = {
@@ -94,7 +96,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
reader = UltraheatReader(port)
heat_meter = HeatMeterService(reader)
try:
async with async_timeout.timeout(10):
async with async_timeout.timeout(ULTRAHEAT_TIMEOUT):
# validate and retrieve the model and device number for a unique id
data = await self.hass.async_add_executor_job(heat_meter.read)
_LOGGER.debug("Got data from Ultraheat API: %s", data)
@@ -11,6 +11,7 @@ from homeassistant.helpers.entity import EntityCategory
DOMAIN = "landisgyr_heat_meter"
GJ_TO_MWH = 0.277778 # conversion factor
ULTRAHEAT_TIMEOUT = 30 # reading the IR port can take some time
HEAT_METER_SENSOR_TYPES = (
SensorEntityDescription(
+4 -2
View File
@@ -6,7 +6,7 @@ from datetime import timedelta
import logging
import async_timeout
from led_ble import BLEAK_EXCEPTIONS, LEDBLE
from led_ble import BLEAK_EXCEPTIONS, LEDBLE, get_device
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
@@ -27,7 +27,9 @@ _LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up LED BLE from a config entry."""
address: str = entry.data[CONF_ADDRESS]
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
ble_device = bluetooth.async_ble_device_from_address(
hass, address.upper(), True
) or await get_device(address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find LED BLE device with address {address}"
+2 -2
View File
@@ -48,12 +48,12 @@ class LEDBLEEntity(CoordinatorEntity, LightEntity):
"""Initialize an ledble light."""
super().__init__(coordinator)
self._device = device
self._attr_unique_id = device._address
self._attr_unique_id = device.address
self._attr_device_info = DeviceInfo(
name=name,
model=hex(device.model_num),
sw_version=hex(device.version_num),
connections={(dr.CONNECTION_BLUETOOTH, device._address)},
connections={(dr.CONNECTION_BLUETOOTH, device.address)},
)
self._async_update_attrs()
@@ -3,7 +3,7 @@
"name": "LED BLE",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ble_ble",
"requirements": ["led-ble==0.7.0"],
"requirements": ["led-ble==0.10.1"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"bluetooth": [
@@ -1,5 +1,5 @@
"""Config flow to configure the LG Soundbar integration."""
from queue import Queue
from queue import Full, Queue
import socket
import temescal
@@ -20,18 +20,29 @@ def test_connect(host, port):
uuid_q = Queue(maxsize=1)
name_q = Queue(maxsize=1)
def queue_add(attr_q, data):
try:
attr_q.put_nowait(data)
except Full:
pass
def msg_callback(response):
if response["msg"] == "MAC_INFO_DEV" and "s_uuid" in response["data"]:
uuid_q.put_nowait(response["data"]["s_uuid"])
if (
response["msg"] in ["MAC_INFO_DEV", "PRODUCT_INFO"]
and "s_uuid" in response["data"]
):
queue_add(uuid_q, response["data"]["s_uuid"])
if (
response["msg"] == "SPK_LIST_VIEW_INFO"
and "s_user_name" in response["data"]
):
name_q.put_nowait(response["data"]["s_user_name"])
queue_add(name_q, response["data"]["s_user_name"])
try:
connection = temescal.temescal(host, port=port, callback=msg_callback)
connection.get_mac_info()
if uuid_q.empty():
connection.get_product_info()
connection.get_info()
details = {"name": name_q.get(timeout=10), "uuid": uuid_q.get(timeout=10)}
return details
+2 -7
View File
@@ -219,15 +219,10 @@ class LIFXLight(LIFXEntity, LightEntity):
elif power_on:
await self.set_power(True, duration=fade)
else:
if power_on:
await self.set_power(True)
if hsbk:
await self.set_color(hsbk, kwargs, duration=fade)
# The response from set_color will tell us if the
# bulb is actually on or not, so we don't need to
# call power_on if its already on
if power_on and self.bulb.power_level == 0:
await self.set_power(True)
elif power_on:
await self.set_power(True)
if power_off:
await self.set_power(False, duration=fade)
@@ -3,7 +3,7 @@
"name": "Litter-Robot",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
"requirements": ["pylitterbot==2022.8.2"],
"requirements": ["pylitterbot==2022.9.1"],
"codeowners": ["@natekspencer", "@tkdrob"],
"dhcp": [{ "hostname": "litter-robot4" }],
"iot_class": "cloud_polling",
+78 -41
View File
@@ -30,6 +30,7 @@ from homeassistant.helpers import (
)
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import (
async_integration_yaml_config,
async_reload_integration_platforms,
@@ -65,13 +66,7 @@ from .const import ( # noqa: F401
CONF_TLS_VERSION,
CONF_TOPIC,
CONF_WILL_MESSAGE,
CONFIG_ENTRY_IS_SETUP,
DATA_MQTT,
DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_DISPATCHERS,
DATA_MQTT_RELOAD_ENTRY,
DATA_MQTT_RELOAD_NEEDED,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING,
DEFAULT_QOS,
DEFAULT_RETAIN,
@@ -81,7 +76,7 @@ from .const import ( # noqa: F401
PLATFORMS,
RELOADABLE_PLATFORMS,
)
from .mixins import async_discover_yaml_entities
from .mixins import MqttData
from .models import ( # noqa: F401
MqttCommandTemplate,
MqttValueTemplate,
@@ -169,6 +164,8 @@ async def _async_setup_discovery(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the MQTT protocol service."""
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
conf: ConfigType | None = config.get(DOMAIN)
websocket_api.async_register_command(hass, websocket_subscribe)
@@ -177,7 +174,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if conf:
conf = dict(conf)
hass.data[DATA_MQTT_CONFIG] = conf
mqtt_data.config = conf
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is None:
# Create an import flow if the user has yaml configured entities etc.
@@ -189,12 +186,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
data={},
)
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
mqtt_data.reload_needed = True
elif mqtt_entry_status is False:
_LOGGER.info(
"MQTT will be not available until the config entry is enabled",
)
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
mqtt_data.reload_needed = True
return True
@@ -252,33 +249,34 @@ async def _async_config_entry_updated(hass: HomeAssistant, entry: ConfigEntry) -
Causes for this is config entry options changing.
"""
mqtt_client = hass.data[DATA_MQTT]
mqtt_data: MqttData = hass.data[DATA_MQTT]
assert (client := mqtt_data.client) is not None
if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None:
if (conf := mqtt_data.config) is None:
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
mqtt_client.conf = _merge_extended_config(entry, conf)
await mqtt_client.async_disconnect()
mqtt_client.init_client()
await mqtt_client.async_connect()
mqtt_data.config = _merge_extended_config(entry, conf)
await client.async_disconnect()
client.init_client()
await client.async_connect()
await discovery.async_stop(hass)
if mqtt_client.conf.get(CONF_DISCOVERY):
await _async_setup_discovery(hass, mqtt_client.conf, entry)
if client.conf.get(CONF_DISCOVERY):
await _async_setup_discovery(hass, cast(ConfigType, mqtt_data.config), entry)
async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict | None:
"""Fetch fresh MQTT yaml config from the hass config when (re)loading the entry."""
if DATA_MQTT_RELOAD_ENTRY in hass.data:
mqtt_data: MqttData = hass.data[DATA_MQTT]
if mqtt_data.reload_entry:
hass_config = await conf_util.async_hass_config_yaml(hass)
mqtt_config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
hass.data[DATA_MQTT_CONFIG] = mqtt_config
mqtt_data.config = CONFIG_SCHEMA_BASE(hass_config.get(DOMAIN, {}))
# Remove unknown keys from config entry data
_filter_entry_config(hass, entry)
# Merge basic configuration, and add missing defaults for basic options
_merge_basic_config(hass, entry, hass.data.get(DATA_MQTT_CONFIG, {}))
_merge_basic_config(hass, entry, mqtt_data.config or {})
# Bail out if broker setting is missing
if CONF_BROKER not in entry.data:
_LOGGER.error("MQTT broker is not configured, please configure it")
@@ -286,7 +284,7 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict |
# If user doesn't have configuration.yaml config, generate default values
# for options not in config entry data
if (conf := hass.data.get(DATA_MQTT_CONFIG)) is None:
if (conf := mqtt_data.config) is None:
conf = CONFIG_SCHEMA_BASE(dict(entry.data))
# User has configuration.yaml config, warn about config entry overrides
@@ -309,15 +307,20 @@ async def async_fetch_config(hass: HomeAssistant, entry: ConfigEntry) -> dict |
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Load a config entry."""
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
# Merge basic configuration, and add missing defaults for basic options
if (conf := await async_fetch_config(hass, entry)) is None:
# Bail out
return False
hass.data[DATA_MQTT] = MQTT(hass, entry, conf)
mqtt_data.client = MQTT(hass, entry, conf)
# Restore saved subscriptions
if mqtt_data.subscriptions_to_restore:
mqtt_data.client.subscriptions = mqtt_data.subscriptions_to_restore
mqtt_data.subscriptions_to_restore = []
entry.add_update_listener(_async_config_entry_updated)
await hass.data[DATA_MQTT].async_connect()
await mqtt_data.client.async_connect()
async def async_publish_service(call: ServiceCall) -> None:
"""Handle MQTT publish service calls."""
@@ -366,7 +369,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return
await hass.data[DATA_MQTT].async_publish(msg_topic, payload, qos, retain)
assert mqtt_data.client is not None and msg_topic is not None
await mqtt_data.client.async_publish(msg_topic, payload, qos, retain)
hass.services.async_register(
DOMAIN, SERVICE_PUBLISH, async_publish_service, schema=MQTT_PUBLISH_SCHEMA
@@ -407,7 +411,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
# setup platforms and discovery
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
async def async_setup_reload_service() -> None:
"""Create the reload service for the MQTT domain."""
@@ -420,13 +423,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
# Reload the modern yaml platforms
mqtt_platforms = async_get_platforms(hass, DOMAIN)
tasks = [
entity.async_remove()
for mqtt_platform in mqtt_platforms
for entity in mqtt_platform.entities.values()
if not entity._discovery_data # type: ignore[attr-defined] # pylint: disable=protected-access
if mqtt_platform.config_entry
and mqtt_platform.domain in RELOADABLE_PLATFORMS
]
await asyncio.gather(*tasks)
config_yaml = await async_integration_yaml_config(hass, DOMAIN) or {}
hass.data[DATA_MQTT_UPDATED_CONFIG] = config_yaml.get(DOMAIN, {})
mqtt_data.updated_config = config_yaml.get(DOMAIN, {})
await asyncio.gather(
*(
[
async_discover_yaml_entities(hass, component)
mqtt_data.reload_handlers[component]()
for component in RELOADABLE_PLATFORMS
if component in mqtt_data.reload_handlers
]
)
)
@@ -438,6 +453,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_forward_entry_setup_and_setup_discovery(config_entry):
"""Forward the config entry setup to the platforms and set up discovery."""
reload_manual_setup: bool = False
# Local import to avoid circular dependencies
# pylint: disable-next=import-outside-toplevel
from . import device_automation, tag
@@ -460,8 +476,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await _async_setup_discovery(hass, conf, entry)
# Setup reload service after all platforms have loaded
await async_setup_reload_service()
if DATA_MQTT_RELOAD_NEEDED in hass.data:
hass.data.pop(DATA_MQTT_RELOAD_NEEDED)
# When the entry is reloaded, also reload manual set up items to enable MQTT
if mqtt_data.reload_entry:
mqtt_data.reload_entry = False
reload_manual_setup = True
# When the entry was disabled before, reload manual set up items to enable MQTT again
if mqtt_data.reload_needed:
mqtt_data.reload_needed = False
reload_manual_setup = True
if reload_manual_setup:
await async_reload_manual_mqtt_items(hass)
await async_forward_entry_setup_and_setup_discovery(entry)
@@ -568,7 +593,9 @@ def async_subscribe_connection_status(
def is_connected(hass: HomeAssistant) -> bool:
"""Return if MQTT client is connected."""
return hass.data[DATA_MQTT].connected
mqtt_data: MqttData = hass.data[DATA_MQTT]
assert mqtt_data.client is not None
return mqtt_data.client.connected
async def async_remove_config_entry_device(
@@ -584,6 +611,10 @@ async def async_remove_config_entry_device(
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload MQTT dump and publish service when the config entry is unloaded."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
assert mqtt_data.client is not None
mqtt_client = mqtt_data.client
# Unload publish and dump services.
hass.services.async_remove(
DOMAIN,
@@ -596,7 +627,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Stop the discovery
await discovery.async_stop(hass)
mqtt_client: MQTT = hass.data[DATA_MQTT]
# Unload the platforms
await asyncio.gather(
*(
@@ -606,23 +636,30 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
await hass.async_block_till_done()
# Unsubscribe reload dispatchers
while reload_dispatchers := hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []):
while reload_dispatchers := mqtt_data.reload_dispatchers:
reload_dispatchers.pop()()
hass.data[CONFIG_ENTRY_IS_SETUP] = set()
# Cleanup listeners
mqtt_client.cleanup()
# Trigger reload manual MQTT items at entry setup
# Reload the legacy yaml platform
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
if (mqtt_entry_status := mqtt_config_entry_enabled(hass)) is False:
# The entry is disabled reload legacy manual items when the entry is enabled again
hass.data[DATA_MQTT_RELOAD_NEEDED] = True
mqtt_data.reload_needed = True
elif mqtt_entry_status is True:
# The entry is reloaded:
# Trigger re-fetching the yaml config at entry setup
hass.data[DATA_MQTT_RELOAD_ENTRY] = True
# Stop the loop
mqtt_data.reload_entry = True
# Reload the legacy yaml platform to make entities unavailable
await async_reload_integration_platforms(hass, DOMAIN, RELOADABLE_PLATFORMS)
# Cleanup entity registry hooks
registry_hooks = mqtt_data.discovery_registry_hooks
while registry_hooks:
registry_hooks.popitem()[1]()
# Wait for all ACKs and stop the loop
await mqtt_client.async_disconnect()
# Store remaining subscriptions to be able to restore or reload them
# when the entry is set up again
if mqtt_client.subscriptions:
mqtt_data.subscriptions_to_restore = mqtt_client.subscriptions
return True
@@ -44,7 +44,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -146,9 +145,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT alarm control panel through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, alarm.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -42,7 +42,6 @@ from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttAvailability,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -102,9 +101,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT binary sensor through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, binary_sensor.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
-4
View File
@@ -25,7 +25,6 @@ from .const import (
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -81,9 +80,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT button through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, button.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
-4
View File
@@ -23,7 +23,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -105,9 +104,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT camera through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, camera.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
+34 -17
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable, Coroutine, Iterable
from collections.abc import Callable, Coroutine, Iterable
from functools import lru_cache, partial, wraps
import inspect
from itertools import groupby
@@ -17,6 +17,7 @@ import attr
import certifi
from paho.mqtt.client import MQTTMessage
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_CLIENT_ID,
CONF_PASSWORD,
@@ -52,7 +53,6 @@ from .const import (
MQTT_DISCONNECTED,
PROTOCOL_31,
)
from .discovery import LAST_DISCOVERY
from .models import (
AsyncMessageCallbackType,
MessageCallbackType,
@@ -68,6 +68,9 @@ if TYPE_CHECKING:
# because integrations should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt
from .mixins import MqttData
_LOGGER = logging.getLogger(__name__)
DISCOVERY_COOLDOWN = 2
@@ -97,8 +100,12 @@ async def async_publish(
encoding: str | None = DEFAULT_ENCODING,
) -> None:
"""Publish message to a MQTT topic."""
# Local import to avoid circular dependencies
# pylint: disable-next=import-outside-toplevel
from .mixins import MqttData
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
if mqtt_data.client is None or not mqtt_config_entry_enabled(hass):
raise HomeAssistantError(
f"Cannot publish to topic '{topic}', MQTT is not enabled"
)
@@ -126,11 +133,13 @@ async def async_publish(
)
return
await hass.data[DATA_MQTT].async_publish(topic, outgoing_payload, qos, retain)
await mqtt_data.client.async_publish(
topic, outgoing_payload, qos or 0, retain or False
)
AsyncDeprecatedMessageCallbackType = Callable[
[str, ReceivePayloadType, int], Awaitable[None]
[str, ReceivePayloadType, int], Coroutine[Any, Any, None]
]
DeprecatedMessageCallbackType = Callable[[str, ReceivePayloadType, int], None]
@@ -175,13 +184,18 @@ async def async_subscribe(
| DeprecatedMessageCallbackType
| AsyncDeprecatedMessageCallbackType,
qos: int = DEFAULT_QOS,
encoding: str | None = "utf-8",
encoding: str | None = DEFAULT_ENCODING,
):
"""Subscribe to an MQTT topic.
Call the return value to unsubscribe.
"""
if DATA_MQTT not in hass.data or not mqtt_config_entry_enabled(hass):
# Local import to avoid circular dependencies
# pylint: disable-next=import-outside-toplevel
from .mixins import MqttData
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
if mqtt_data.client is None or not mqtt_config_entry_enabled(hass):
raise HomeAssistantError(
f"Cannot subscribe to topic '{topic}', MQTT is not enabled"
)
@@ -206,7 +220,7 @@ async def async_subscribe(
cast(DeprecatedMessageCallbackType, msg_callback)
)
async_remove = await hass.data[DATA_MQTT].async_subscribe(
async_remove = await mqtt_data.client.async_subscribe(
topic,
catch_log_exception(
wrapped_msg_callback,
@@ -310,14 +324,16 @@ class MQTT:
def __init__(
self,
hass: HomeAssistant,
config_entry,
conf,
config_entry: ConfigEntry,
conf: ConfigType,
) -> None:
"""Initialize Home Assistant MQTT client."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
self._mqtt_data: MqttData = hass.data[DATA_MQTT]
self.hass = hass
self.config_entry = config_entry
self.conf = conf
@@ -435,12 +451,13 @@ class MQTT:
"""Return False if there are unprocessed ACKs."""
return not bool(self._pending_operations)
# wait for ACK-s to be processesed (unsubscribe only)
# wait for ACKs to be processed
async with self._pending_operations_condition:
await self._pending_operations_condition.wait_for(no_more_acks)
# stop the MQTT loop
await self.hass.async_add_executor_job(stop)
async with self._paho_lock:
await self.hass.async_add_executor_job(stop)
async def async_subscribe(
self,
@@ -501,7 +518,8 @@ class MQTT:
async with self._paho_lock:
mid = await self.hass.async_add_executor_job(_client_unsubscribe, topic)
await self._register_mid(mid)
self.hass.async_create_task(self._wait_for_mid(mid))
self.hass.async_create_task(self._wait_for_mid(mid))
async def _async_perform_subscriptions(
self, subscriptions: Iterable[tuple[str, int]]
@@ -632,7 +650,6 @@ class MQTT:
subscription.job,
)
continue
self.hass.async_run_hass_job(
subscription.job,
ReceiveMessage(
@@ -692,10 +709,10 @@ class MQTT:
async def _discovery_cooldown(self):
now = time.time()
# Reset discovery and subscribe cooldowns
self.hass.data[LAST_DISCOVERY] = now
self._mqtt_data.last_discovery = now
self._last_subscribe = now
last_discovery = self.hass.data[LAST_DISCOVERY]
last_discovery = self._mqtt_data.last_discovery
last_subscribe = self._last_subscribe
wait_until = max(
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
@@ -703,7 +720,7 @@ class MQTT:
while now < wait_until:
await asyncio.sleep(wait_until - now)
now = time.time()
last_discovery = self.hass.data[LAST_DISCOVERY]
last_discovery = self._mqtt_data.last_discovery
last_subscribe = self._last_subscribe
wait_until = max(
last_discovery + DISCOVERY_COOLDOWN, last_subscribe + DISCOVERY_COOLDOWN
-4
View File
@@ -50,7 +50,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -350,9 +349,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT climate device through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, climate.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
+18 -7
View File
@@ -18,7 +18,7 @@ from homeassistant.const import (
CONF_PROTOCOL,
CONF_USERNAME,
)
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import FlowResult
from .client import MqttClientSetup
@@ -30,12 +30,13 @@ from .const import (
CONF_BIRTH_MESSAGE,
CONF_BROKER,
CONF_WILL_MESSAGE,
DATA_MQTT_CONFIG,
DATA_MQTT,
DEFAULT_BIRTH,
DEFAULT_DISCOVERY,
DEFAULT_WILL,
DOMAIN,
)
from .mixins import MqttData
from .util import MQTT_WILL_BIRTH_SCHEMA
MQTT_TIMEOUT = 5
@@ -164,9 +165,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the MQTT broker configuration."""
mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData())
errors = {}
current_config = self.config_entry.data
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
yaml_config = mqtt_data.config or {}
if user_input is not None:
can_connect = await self.hass.async_add_executor_job(
try_connection,
@@ -214,9 +216,10 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the MQTT options."""
mqtt_data: MqttData = self.hass.data.setdefault(DATA_MQTT, MqttData())
errors = {}
current_config = self.config_entry.data
yaml_config = self.hass.data.get(DATA_MQTT_CONFIG, {})
yaml_config = mqtt_data.config or {}
options_config: dict[str, Any] = {}
if user_input is not None:
bad_birth = False
@@ -334,14 +337,22 @@ class MQTTOptionsFlowHandler(config_entries.OptionsFlow):
)
def try_connection(hass, broker, port, username, password, protocol="3.1"):
def try_connection(
hass: HomeAssistant,
broker: str,
port: int,
username: str | None,
password: str | None,
protocol: str = "3.1",
) -> bool:
"""Test if we can connect to an MQTT broker."""
# We don't import on the top because some integrations
# should be able to optionally rely on MQTT.
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
# Get the config from configuration.yaml
yaml_config = hass.data.get(DATA_MQTT_CONFIG, {})
mqtt_data: MqttData = hass.data.setdefault(DATA_MQTT, MqttData())
yaml_config = mqtt_data.config or {}
entry_config = {
CONF_BROKER: broker,
CONF_PORT: port,
@@ -351,7 +362,7 @@ def try_connection(hass, broker, port, username, password, protocol="3.1"):
}
client = MqttClientSetup({**yaml_config, **entry_config}).client
result = queue.Queue(maxsize=1)
result: queue.Queue[bool] = queue.Queue(maxsize=1)
def on_connect(client_, userdata, flags, result_code):
"""Handle connection result."""
-6
View File
@@ -30,14 +30,8 @@ CONF_CLIENT_CERT = "client_cert"
CONF_TLS_INSECURE = "tls_insecure"
CONF_TLS_VERSION = "tls_version"
CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup"
DATA_MQTT = "mqtt"
DATA_MQTT_CONFIG = "mqtt_config"
MQTT_DATA_DEVICE_TRACKER_LEGACY = "mqtt_device_tracker_legacy"
DATA_MQTT_RELOAD_DISPATCHERS = "mqtt_reload_dispatchers"
DATA_MQTT_RELOAD_ENTRY = "mqtt_reload_entry"
DATA_MQTT_RELOAD_NEEDED = "mqtt_reload_needed"
DATA_MQTT_UPDATED_CONFIG = "mqtt_updated_config"
DEFAULT_PREFIX = "homeassistant"
DEFAULT_BIRTH_WILL_TOPIC = DEFAULT_PREFIX + "/status"
-4
View File
@@ -46,7 +46,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -242,9 +241,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT cover through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, cover.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
+18 -17
View File
@@ -33,11 +33,13 @@ from .const import (
CONF_PAYLOAD,
CONF_QOS,
CONF_TOPIC,
DATA_MQTT,
DOMAIN,
)
from .discovery import MQTT_DISCOVERY_DONE
from .mixins import (
MQTT_ENTITY_DEVICE_INFO_SCHEMA,
MqttData,
MqttDiscoveryDeviceUpdate,
send_discovery_done,
update_device,
@@ -81,8 +83,6 @@ TRIGGER_DISCOVERY_SCHEMA = MQTT_BASE_SCHEMA.extend(
extra=vol.REMOVE_EXTRA,
)
DEVICE_TRIGGERS = "mqtt_device_triggers"
LOG_NAME = "Device trigger"
@@ -203,6 +203,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
self.device_id = device_id
self.discovery_data = discovery_data
self.hass = hass
self._mqtt_data: MqttData = hass.data[DATA_MQTT]
MqttDiscoveryDeviceUpdate.__init__(
self,
@@ -217,8 +218,8 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
"""Initialize the device trigger."""
discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
discovery_id = discovery_hash[1]
if discovery_id not in self.hass.data.setdefault(DEVICE_TRIGGERS, {}):
self.hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
if discovery_id not in self._mqtt_data.device_triggers:
self._mqtt_data.device_triggers[discovery_id] = Trigger(
hass=self.hass,
device_id=self.device_id,
discovery_data=self.discovery_data,
@@ -230,7 +231,7 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
value_template=self._config[CONF_VALUE_TEMPLATE],
)
else:
await self.hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger(
await self._mqtt_data.device_triggers[discovery_id].update_trigger(
self._config
)
debug_info.add_trigger_discovery_data(
@@ -246,16 +247,16 @@ class MqttDeviceTrigger(MqttDiscoveryDeviceUpdate):
)
config = TRIGGER_DISCOVERY_SCHEMA(discovery_data)
update_device(self.hass, self._config_entry, config)
device_trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id]
device_trigger: Trigger = self._mqtt_data.device_triggers[discovery_id]
await device_trigger.update_trigger(config)
async def async_tear_down(self) -> None:
"""Cleanup device trigger."""
discovery_hash = self.discovery_data[ATTR_DISCOVERY_HASH]
discovery_id = discovery_hash[1]
if discovery_id in self.hass.data[DEVICE_TRIGGERS]:
if discovery_id in self._mqtt_data.device_triggers:
_LOGGER.info("Removing trigger: %s", discovery_hash)
trigger: Trigger = self.hass.data[DEVICE_TRIGGERS][discovery_id]
trigger: Trigger = self._mqtt_data.device_triggers[discovery_id]
trigger.detach_trigger()
debug_info.remove_trigger_discovery_data(self.hass, discovery_hash)
@@ -280,11 +281,10 @@ async def async_setup_trigger(
async def async_removed_from_device(hass: HomeAssistant, device_id: str) -> None:
"""Handle Mqtt removed from a device."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
triggers = await async_get_triggers(hass, device_id)
for trig in triggers:
device_trigger: Trigger = hass.data[DEVICE_TRIGGERS].pop(
trig[CONF_DISCOVERY_ID]
)
device_trigger: Trigger = mqtt_data.device_triggers.pop(trig[CONF_DISCOVERY_ID])
if device_trigger:
device_trigger.detach_trigger()
discovery_data = cast(dict, device_trigger.discovery_data)
@@ -296,12 +296,13 @@ async def async_get_triggers(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
"""List device triggers for MQTT devices."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
triggers: list[dict[str, str]] = []
if DEVICE_TRIGGERS not in hass.data:
if not mqtt_data.device_triggers:
return triggers
for discovery_id, trig in hass.data[DEVICE_TRIGGERS].items():
for discovery_id, trig in mqtt_data.device_triggers.items():
if trig.device_id != device_id or trig.topic is None:
continue
@@ -324,12 +325,12 @@ async def async_attach_trigger(
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
hass.data.setdefault(DEVICE_TRIGGERS, {})
mqtt_data: MqttData = hass.data[DATA_MQTT]
device_id = config[CONF_DEVICE_ID]
discovery_id = config[CONF_DISCOVERY_ID]
if discovery_id not in hass.data[DEVICE_TRIGGERS]:
hass.data[DEVICE_TRIGGERS][discovery_id] = Trigger(
if discovery_id not in mqtt_data.device_triggers:
mqtt_data.device_triggers[discovery_id] = Trigger(
hass=hass,
device_id=device_id,
discovery_data=None,
@@ -340,6 +341,6 @@ async def async_attach_trigger(
qos=None,
value_template=None,
)
return await hass.data[DEVICE_TRIGGERS][discovery_id].add_trigger(
return await mqtt_data.device_triggers[discovery_id].add_trigger(
action, trigger_info
)
+1 -1
View File
@@ -43,7 +43,7 @@ def _async_get_diagnostics(
device: DeviceEntry | None = None,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
mqtt_instance: MQTT = hass.data[DATA_MQTT]
mqtt_instance: MQTT = hass.data[DATA_MQTT].client
redacted_config = async_redact_data(mqtt_instance.conf, REDACT_CONFIG)
+10 -5
View File
@@ -7,6 +7,7 @@ import functools
import logging
import re
import time
from typing import TYPE_CHECKING
from homeassistant.const import CONF_DEVICE, CONF_PLATFORM
from homeassistant.core import HomeAssistant
@@ -28,9 +29,13 @@ from .const import (
ATTR_DISCOVERY_TOPIC,
CONF_AVAILABILITY,
CONF_TOPIC,
DATA_MQTT,
DOMAIN,
)
if TYPE_CHECKING:
from .mixins import MqttData
_LOGGER = logging.getLogger(__name__)
TOPIC_MATCHER = re.compile(
@@ -69,7 +74,6 @@ INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe"
MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}"
MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}"
MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}"
LAST_DISCOVERY = "mqtt_last_discovery"
TOPIC_BASE = "~"
@@ -80,12 +84,12 @@ class MQTTConfig(dict):
discovery_data: dict
def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple) -> None:
def clear_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]) -> None:
"""Clear entry in ALREADY_DISCOVERED list."""
del hass.data[ALREADY_DISCOVERED][discovery_hash]
def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple):
def set_discovery_hash(hass: HomeAssistant, discovery_hash: tuple[str, str]):
"""Clear entry in ALREADY_DISCOVERED list."""
hass.data[ALREADY_DISCOVERED][discovery_hash] = {}
@@ -94,11 +98,12 @@ async def async_start( # noqa: C901
hass: HomeAssistant, discovery_topic, config_entry=None
) -> None:
"""Start MQTT Discovery."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
mqtt_integrations = {}
async def async_discovery_message_received(msg):
"""Process the received message."""
hass.data[LAST_DISCOVERY] = time.time()
mqtt_data.last_discovery = time.time()
payload = msg.payload
topic = msg.topic
topic_trimmed = topic.replace(f"{discovery_topic}/", "", 1)
@@ -253,7 +258,7 @@ async def async_start( # noqa: C901
)
)
hass.data[LAST_DISCOVERY] = time.time()
mqtt_data.last_discovery = time.time()
mqtt_integrations = await async_get_mqtt(hass)
hass.data[INTEGRATION_UNSUBSCRIBE] = {}
-4
View File
@@ -50,7 +50,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -241,9 +240,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT fan through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, fan.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -46,7 +46,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -187,9 +186,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT humidifier through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, humidifier.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -14,7 +14,6 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from ..mixins import (
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -111,9 +110,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT lights configured under the light platform key (deprecated)."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, light.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -249,7 +249,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid RGB color value received")
_LOGGER.warning(
"Invalid RGB color value received for entity %s", self.entity_id
)
return
try:
@@ -259,7 +261,9 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid XY color value received")
_LOGGER.warning(
"Invalid XY color value received for entity %s", self.entity_id
)
return
try:
@@ -269,12 +273,16 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid HS color value received")
_LOGGER.warning(
"Invalid HS color value received for entity %s", self.entity_id
)
return
else:
color_mode = values["color_mode"]
if not self._supports_color_mode(color_mode):
_LOGGER.warning("Invalid color mode received")
_LOGGER.warning(
"Invalid color mode received for entity %s", self.entity_id
)
return
try:
if color_mode == ColorMode.COLOR_TEMP:
@@ -314,7 +322,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._color_mode = ColorMode.XY
self._xy = (x, y)
except (KeyError, ValueError):
_LOGGER.warning("Invalid or incomplete color value received")
_LOGGER.warning(
"Invalid or incomplete color value received for entity %s",
self.entity_id,
)
def _prepare_subscribe_topics(self):
"""(Re)Subscribe to topics."""
@@ -351,7 +362,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
except KeyError:
pass
except (TypeError, ValueError):
_LOGGER.warning("Invalid brightness value received")
_LOGGER.warning(
"Invalid brightness value received for entity %s",
self.entity_id,
)
if (
self._supported_features
@@ -366,7 +380,10 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
except KeyError:
pass
except ValueError:
_LOGGER.warning("Invalid color temp value received")
_LOGGER.warning(
"Invalid color temp value received for entity %s",
self.entity_id,
)
if self._supported_features and LightEntityFeature.EFFECT:
with suppress(KeyError):
-4
View File
@@ -28,7 +28,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -102,9 +101,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT lock through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, lock.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
+114 -40
View File
@@ -4,9 +4,10 @@ from __future__ import annotations
from abc import abstractmethod
import asyncio
from collections.abc import Callable, Coroutine
from dataclasses import dataclass, field
from functools import partial
import logging
from typing import Any, Protocol, cast, final
from typing import TYPE_CHECKING, Any, Protocol, cast, final
import voluptuous as vol
@@ -28,11 +29,16 @@ from homeassistant.const import (
CONF_UNIQUE_ID,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import Event, HomeAssistant, async_get_hass, callback
from homeassistant.core import (
CALLBACK_TYPE,
Event,
HomeAssistant,
async_get_hass,
callback,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
discovery,
entity_registry as er,
)
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
@@ -48,12 +54,13 @@ from homeassistant.helpers.entity import (
async_generate_entity_id,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_entity_registry_updated_event
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.json import json_loads
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import debug_info, subscription
from .client import async_publish
from .client import MQTT, Subscription, async_publish
from .const import (
ATTR_DISCOVERY_HASH,
ATTR_DISCOVERY_PAYLOAD,
@@ -63,9 +70,6 @@ from .const import (
CONF_QOS,
CONF_TOPIC,
DATA_MQTT,
DATA_MQTT_CONFIG,
DATA_MQTT_RELOAD_DISPATCHERS,
DATA_MQTT_UPDATED_CONFIG,
DEFAULT_ENCODING,
DEFAULT_PAYLOAD_AVAILABLE,
DEFAULT_PAYLOAD_NOT_AVAILABLE,
@@ -89,6 +93,9 @@ from .subscription import (
)
from .util import mqtt_config_entry_enabled, valid_subscribe_topic
if TYPE_CHECKING:
from .device_trigger import Trigger
_LOGGER = logging.getLogger(__name__)
AVAILABILITY_ALL = "all"
@@ -265,6 +272,27 @@ def warn_for_legacy_schema(domain: str) -> Callable:
return validator
@dataclass
class MqttData:
"""Keep the MQTT entry data."""
client: MQTT | None = None
config: ConfigType | None = None
device_triggers: dict[str, Trigger] = field(default_factory=dict)
discovery_registry_hooks: dict[tuple[str, str], CALLBACK_TYPE] = field(
default_factory=dict
)
last_discovery: float = 0.0
reload_dispatchers: list[CALLBACK_TYPE] = field(default_factory=list)
reload_entry: bool = False
reload_handlers: dict[str, Callable[[], Coroutine[Any, Any, None]]] = field(
default_factory=dict
)
reload_needed: bool = False
subscriptions_to_restore: list[Subscription] = field(default_factory=list)
updated_config: ConfigType = field(default_factory=dict)
class SetupEntity(Protocol):
"""Protocol type for async_setup_entities."""
@@ -279,29 +307,6 @@ class SetupEntity(Protocol):
"""Define setup_entities type."""
async def async_discover_yaml_entities(
hass: HomeAssistant, platform_domain: str
) -> None:
"""Discover entities for a platform."""
if DATA_MQTT_UPDATED_CONFIG in hass.data:
# The platform has been reloaded
config_yaml = hass.data[DATA_MQTT_UPDATED_CONFIG]
else:
config_yaml = hass.data.get(DATA_MQTT_CONFIG, {})
if not config_yaml:
return
if platform_domain not in config_yaml:
return
await asyncio.gather(
*(
discovery.async_load_platform(hass, platform_domain, DOMAIN, config, {})
for config in await async_get_platform_config_from_yaml(
hass, platform_domain, config_yaml
)
)
)
async def async_get_platform_config_from_yaml(
hass: HomeAssistant,
platform_domain: str,
@@ -309,8 +314,9 @@ async def async_get_platform_config_from_yaml(
) -> list[ConfigType]:
"""Return a list of validated configurations for the domain."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
if config_yaml is None:
config_yaml = hass.data.get(DATA_MQTT_CONFIG)
config_yaml = mqtt_data.config
if not config_yaml:
return []
if not (platform_configs := config_yaml.get(platform_domain)):
@@ -322,9 +328,10 @@ async def async_setup_entry_helper(
hass: HomeAssistant,
domain: str,
async_setup: partial[Coroutine[HomeAssistant, str, None]],
schema: vol.Schema,
discovery_schema: vol.Schema,
) -> None:
"""Set up entity, automation or tag creation dynamically through MQTT discovery."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
async def async_discover(discovery_payload):
"""Discover and add an MQTT entity, automation or tag."""
@@ -338,7 +345,7 @@ async def async_setup_entry_helper(
return
discovery_data = discovery_payload.discovery_data
try:
config = schema(discovery_payload)
config = discovery_schema(discovery_payload)
await async_setup(config, discovery_data=discovery_data)
except Exception:
discovery_hash = discovery_data[ATTR_DISCOVERY_HASH]
@@ -348,12 +355,37 @@ async def async_setup_entry_helper(
)
raise
hass.data.setdefault(DATA_MQTT_RELOAD_DISPATCHERS, []).append(
mqtt_data.reload_dispatchers.append(
async_dispatcher_connect(
hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover
)
)
async def _async_setup_entities() -> None:
"""Set up MQTT items from configuration.yaml."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
if mqtt_data.updated_config:
# The platform has been reloaded
config_yaml = mqtt_data.updated_config
else:
config_yaml = mqtt_data.config or {}
if not config_yaml:
return
if domain not in config_yaml:
return
await asyncio.gather(
*[
async_setup(config)
for config in await async_get_platform_config_from_yaml(
hass, domain, config_yaml
)
]
)
# discover manual configured MQTT items
mqtt_data.reload_handlers[domain] = _async_setup_entities
await _async_setup_entities()
async def async_setup_platform_helper(
hass: HomeAssistant,
@@ -363,6 +395,13 @@ async def async_setup_platform_helper(
async_setup_entities: SetupEntity,
) -> None:
"""Help to set up the platform for manual configured MQTT entities."""
mqtt_data: MqttData = hass.data[DATA_MQTT]
if mqtt_data.reload_entry:
_LOGGER.debug(
"MQTT integration is %s, skipping setup of manually configured MQTT items while unloading the config entry",
platform_domain,
)
return
if not (entry_status := mqtt_config_entry_enabled(hass)):
_LOGGER.warning(
"MQTT integration is %s, skipping setup of manually configured MQTT %s",
@@ -582,7 +621,10 @@ class MqttAvailability(Entity):
@property
def available(self) -> bool:
"""Return if the device is available."""
if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping:
mqtt_data: MqttData = self.hass.data[DATA_MQTT]
assert mqtt_data.client is not None
client = mqtt_data.client
if not client.connected and not self.hass.is_stopping:
return False
if not self._avail_topics:
return True
@@ -617,7 +659,7 @@ async def cleanup_device_registry(
)
def get_discovery_hash(discovery_data: dict) -> tuple:
def get_discovery_hash(discovery_data: dict) -> tuple[str, str]:
"""Get the discovery hash from the discovery data."""
return discovery_data[ATTR_DISCOVERY_HASH]
@@ -647,6 +689,17 @@ async def async_remove_discovery_payload(hass: HomeAssistant, discovery_data: di
await async_publish(hass, discovery_topic, "", retain=True)
async def async_clear_discovery_topic_if_entity_removed(
hass: HomeAssistant,
discovery_data: dict[str, Any],
event: Event,
) -> None:
"""Clear the discovery topic if the entity is removed."""
if event.data["action"] == "remove":
# publish empty payload to config topic to avoid re-adding
await async_remove_discovery_payload(hass, discovery_data)
class MqttDiscoveryDeviceUpdate:
"""Add support for auto discovery for platforms without an entity."""
@@ -780,7 +833,8 @@ class MqttDiscoveryUpdate(Entity):
def __init__(
self,
discovery_data: dict,
hass: HomeAssistant,
discovery_data: dict | None,
discovery_update: Callable | None = None,
) -> None:
"""Initialize the discovery update mixin."""
@@ -788,6 +842,13 @@ class MqttDiscoveryUpdate(Entity):
self._discovery_update = discovery_update
self._remove_discovery_updated: Callable | None = None
self._removed_from_hass = False
if discovery_data is None:
return
mqtt_data: MqttData = hass.data[DATA_MQTT]
self._registry_hooks = mqtt_data.discovery_registry_hooks
discovery_hash: tuple[str, str] = discovery_data[ATTR_DISCOVERY_HASH]
if discovery_hash in self._registry_hooks:
self._registry_hooks.pop(discovery_hash)()
async def async_added_to_hass(self) -> None:
"""Subscribe to discovery updates."""
@@ -850,7 +911,7 @@ class MqttDiscoveryUpdate(Entity):
async def async_removed_from_registry(self) -> None:
"""Clear retained discovery topic in broker."""
if not self._removed_from_hass:
if not self._removed_from_hass and self._discovery_data is not None:
# Stop subscribing to discovery updates to not trigger when we clear the
# discovery topic
self._cleanup_discovery_on_remove()
@@ -861,7 +922,20 @@ class MqttDiscoveryUpdate(Entity):
@callback
def add_to_platform_abort(self) -> None:
"""Abort adding an entity to a platform."""
if self._discovery_data:
if self._discovery_data is not None:
discovery_hash: tuple[str, str] = self._discovery_data[ATTR_DISCOVERY_HASH]
if self.registry_entry is not None:
self._registry_hooks[
discovery_hash
] = async_track_entity_registry_updated_event(
self.hass,
self.entity_id,
partial(
async_clear_discovery_topic_if_entity_removed,
self.hass,
self._discovery_data,
),
)
stop_discovery_updates(self.hass, self._discovery_data)
send_discovery_done(self.hass, self._discovery_data)
super().add_to_platform_abort()
@@ -969,7 +1043,7 @@ class MqttEntity(
# Initialize mixin classes
MqttAttributes.__init__(self, config)
MqttAvailability.__init__(self, config)
MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update)
MqttDiscoveryUpdate.__init__(self, hass, discovery_data, self.discovery_update)
MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry)
def _init_entity_id(self):
-4
View File
@@ -44,7 +44,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -138,9 +137,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT number through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, number.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
-4
View File
@@ -22,7 +22,6 @@ from .mixins import (
CONF_OBJECT_ID,
MQTT_AVAILABILITY_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -78,9 +77,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT scene through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, scene.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
-4
View File
@@ -30,7 +30,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -93,9 +92,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT select through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, select.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
+4 -8
View File
@@ -1,7 +1,7 @@
"""Support for MQTT sensors."""
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import functools
import logging
@@ -30,7 +30,7 @@ from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType
from homeassistant.util import dt as dt_util
from . import subscription
@@ -41,7 +41,6 @@ from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttAvailability,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -146,9 +145,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT sensor through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, sensor.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -346,7 +342,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
self.async_write_ha_state()
@property
def native_unit_of_measurement(self):
def native_unit_of_measurement(self) -> str | None:
"""Return the unit this state is expressed in."""
return self._config.get(CONF_UNIT_OF_MEASUREMENT)
@@ -356,7 +352,7 @@ class MqttSensor(MqttEntity, RestoreSensor):
return self._config[CONF_FORCE_UPDATE]
@property
def native_value(self):
def native_value(self) -> StateType | datetime:
"""Return the state of the entity."""
return self._state
-4
View File
@@ -51,7 +51,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -142,9 +141,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT siren through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, siren.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
-4
View File
@@ -42,7 +42,6 @@ from .debug_info import log_messages
from .mixins import (
MQTT_ENTITY_COMMON_SCHEMA,
MqttEntity,
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
warn_for_legacy_schema,
@@ -101,9 +100,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT switch through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, switch.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -11,11 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from ..mixins import (
async_discover_yaml_entities,
async_setup_entry_helper,
async_setup_platform_helper,
)
from ..mixins import async_setup_entry_helper, async_setup_platform_helper
from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE
from .schema_legacy import (
DISCOVERY_SCHEMA_LEGACY,
@@ -90,9 +86,6 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up MQTT vacuum through configuration.yaml and dynamically through MQTT discovery."""
# load and initialize platform config from configuration.yaml
await async_discover_yaml_entities(hass, vacuum.DOMAIN)
# setup for discovery
setup = functools.partial(
_async_setup_entity, hass, async_add_entities, config_entry=config_entry
)
@@ -138,8 +138,8 @@ class NetatmoDataHandler:
@callback
def async_force_update(self, signal_name: str) -> None:
"""Prioritize data retrieval for given data class entry."""
self.publisher[signal_name].next_scan = time()
self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
# self.publisher[signal_name].next_scan = time()
# self._queue.rotate(-(self._queue.index(self.publisher[signal_name])))
async def handle_event(self, event: dict) -> None:
"""Handle webhook events."""
@@ -130,4 +130,4 @@ class OpenWeatherMapOptionsFlow(config_entries.OptionsFlow):
async def _is_owm_api_online(hass, api_key, lat, lon):
owm = OWM(api_key).weather_manager()
return await hass.async_add_executor_job(owm.one_call, lat, lon)
return await hass.async_add_executor_job(owm.weather_at_coords, lat, lon)
@@ -373,7 +373,7 @@ class Luminary(LightEntity):
self._max_mireds = color_util.color_temperature_kelvin_to_mired(
self._luminary.min_temp() or DEFAULT_KELVIN
)
if len(self._attr_supported_color_modes == 1):
if len(self._attr_supported_color_modes) == 1:
# The light supports only a single color mode
self._attr_color_mode = list(self._attr_supported_color_modes)[0]
@@ -392,7 +392,7 @@ class Luminary(LightEntity):
if ColorMode.HS in self._attr_supported_color_modes:
self._rgb_color = self._luminary.rgb()
if len(self._attr_supported_color_modes > 1):
if len(self._attr_supported_color_modes) > 1:
# The light supports hs + color temp, determine which one it is
if self._rgb_color == (0, 0, 0):
self._attr_color_mode = ColorMode.COLOR_TEMP
@@ -91,7 +91,7 @@ class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN):
self.discovery_info = discovery_info
_properties = discovery_info.properties
unique_id = discovery_info.hostname.split(".")[0]
unique_id = discovery_info.hostname.split(".")[0].split("-")[0]
if config_entry := await self.async_set_unique_id(unique_id):
try:
await validate_gw_input(
+1 -1
View File
@@ -128,7 +128,7 @@ class PushoverNotificationService(BaseNotificationService):
self.pushover.send_message(
self._user_key,
message,
kwargs.get(ATTR_TARGET),
",".join(kwargs.get(ATTR_TARGET, [])),
title,
url,
url_title,
@@ -11,8 +11,8 @@
"connectable": false
}
],
"requirements": ["qingping-ble==0.6.0"],
"requirements": ["qingping-ble==0.7.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"codeowners": ["@bdraco", "@skgsergio"],
"iot_class": "local_push"
}
@@ -2,14 +2,15 @@
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from datetime import timedelta
from functools import partial
from functools import partial, wraps
from typing import Any
from regenmaschine import Client
from regenmaschine.controller import Controller
from regenmaschine.errors import RainMachineError
from regenmaschine.errors import RainMachineError, UnknownAPICallError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
@@ -22,7 +23,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
@@ -152,9 +153,9 @@ class RainMachineData:
@callback
def async_get_controller_for_service_call(
def async_get_entry_for_service_call(
hass: HomeAssistant, call: ServiceCall
) -> Controller:
) -> ConfigEntry:
"""Get the controller related to a service call (by device ID)."""
device_id = call.data[CONF_DEVICE_ID]
device_registry = dr.async_get(hass)
@@ -166,8 +167,7 @@ def async_get_controller_for_service_call(
if (entry := hass.config_entries.async_get_entry(entry_id)) is None:
continue
if entry.domain == DOMAIN:
data: RainMachineData = hass.data[DOMAIN][entry_id]
return data.controller
return entry
raise ValueError(f"No controller for device ID: {device_id}")
@@ -190,7 +190,9 @@ async def async_update_programs_and_zones(
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry( # noqa: C901
hass: HomeAssistant, entry: ConfigEntry
) -> bool:
"""Set up RainMachine as config entry."""
websession = aiohttp_client.async_get_clientsession(hass)
client = Client(session=websession)
@@ -244,6 +246,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
data = await controller.restrictions.universal()
else:
data = await controller.zones.all(details=True, include_inactive=True)
except UnknownAPICallError:
LOGGER.info(
"Skipping unsupported API call for controller %s: %s",
controller.name,
api_category,
)
except RainMachineError as err:
raise UpdateFailed(err) from err
@@ -280,15 +288,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
async def async_pause_watering(call: ServiceCall) -> None:
"""Pause watering for a set number of seconds."""
controller = async_get_controller_for_service_call(hass, call)
await controller.watering.pause_all(call.data[CONF_SECONDS])
await async_update_programs_and_zones(hass, entry)
def call_with_controller(update_programs_and_zones: bool = True) -> Callable:
"""Hydrate a service call with the appropriate controller."""
async def async_push_weather_data(call: ServiceCall) -> None:
def decorator(func: Callable) -> Callable[..., Awaitable]:
"""Define the decorator."""
@wraps(func)
async def wrapper(call: ServiceCall) -> None:
"""Wrap the service function."""
entry = async_get_entry_for_service_call(hass, call)
data: RainMachineData = hass.data[DOMAIN][entry.entry_id]
try:
await func(call, data.controller)
except RainMachineError as err:
raise HomeAssistantError(
f"Error while executing {func.__name__}: {err}"
) from err
if update_programs_and_zones:
await async_update_programs_and_zones(hass, entry)
return wrapper
return decorator
@call_with_controller()
async def async_pause_watering(call: ServiceCall, controller: Controller) -> None:
"""Pause watering for a set number of seconds."""
await controller.watering.pause_all(call.data[CONF_SECONDS])
@call_with_controller(update_programs_and_zones=False)
async def async_push_weather_data(
call: ServiceCall, controller: Controller
) -> None:
"""Push weather data to the device."""
controller = async_get_controller_for_service_call(hass, call)
await controller.parsers.post_data(
{
CONF_WEATHER: [
@@ -301,9 +336,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
}
)
async def async_restrict_watering(call: ServiceCall) -> None:
@call_with_controller()
async def async_restrict_watering(
call: ServiceCall, controller: Controller
) -> None:
"""Restrict watering for a time period."""
controller = async_get_controller_for_service_call(hass, call)
duration = call.data[CONF_DURATION]
await controller.restrictions.set_universal(
{
@@ -311,30 +348,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"rainDelayDuration": duration.total_seconds(),
},
)
await async_update_programs_and_zones(hass, entry)
async def async_stop_all(call: ServiceCall) -> None:
@call_with_controller()
async def async_stop_all(call: ServiceCall, controller: Controller) -> None:
"""Stop all watering."""
controller = async_get_controller_for_service_call(hass, call)
await controller.watering.stop_all()
await async_update_programs_and_zones(hass, entry)
async def async_unpause_watering(call: ServiceCall) -> None:
@call_with_controller()
async def async_unpause_watering(call: ServiceCall, controller: Controller) -> None:
"""Unpause watering."""
controller = async_get_controller_for_service_call(hass, call)
await controller.watering.unpause_all()
await async_update_programs_and_zones(hass, entry)
async def async_unrestrict_watering(call: ServiceCall) -> None:
@call_with_controller()
async def async_unrestrict_watering(
call: ServiceCall, controller: Controller
) -> None:
"""Unrestrict watering."""
controller = async_get_controller_for_service_call(hass, call)
await controller.restrictions.set_universal(
{
"rainDelayStartTime": round(as_timestamp(utcnow())),
"rainDelayDuration": 0,
},
)
await async_update_programs_and_zones(hass, entry)
for service_name, schema, method in (
(
@@ -175,7 +175,9 @@ class ProvisionSettingsBinarySensor(RainMachineEntity, BinarySensorEntity):
def update_from_latest_data(self) -> None:
"""Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR:
self._attr_is_on = self.coordinator.data["system"].get("useFlowSensor")
self._attr_is_on = self.coordinator.data.get("system", {}).get(
"useFlowSensor"
)
class UniversalRestrictionsBinarySensor(RainMachineEntity, BinarySensorEntity):
@@ -3,7 +3,7 @@
"name": "RainMachine",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/rainmachine",
"requirements": ["regenmaschine==2022.08.0"],
"requirements": ["regenmaschine==2022.09.1"],
"codeowners": ["@bachya"],
"iot_class": "local_polling",
"homekit": {
@@ -273,12 +273,14 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
def update_from_latest_data(self) -> None:
"""Update the state."""
if self.entity_description.key == TYPE_FLOW_SENSOR_CLICK_M3:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter"
)
elif self.entity_description.key == TYPE_FLOW_SENSOR_CONSUMED_LITERS:
clicks = self.coordinator.data["system"].get("flowSensorWateringClicks")
clicks_per_m3 = self.coordinator.data["system"].get(
clicks = self.coordinator.data.get("system", {}).get(
"flowSensorWateringClicks"
)
clicks_per_m3 = self.coordinator.data.get("system", {}).get(
"flowSensorClicksPerCubicMeter"
)
@@ -287,11 +289,11 @@ class ProvisionSettingsSensor(RainMachineEntity, SensorEntity):
else:
self._attr_native_value = None
elif self.entity_description.key == TYPE_FLOW_SENSOR_START_INDEX:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorStartIndex"
)
elif self.entity_description.key == TYPE_FLOW_SENSOR_WATERING_CLICKS:
self._attr_native_value = self.coordinator.data["system"].get(
self._attr_native_value = self.coordinator.data.get("system", {}).get(
"flowSensorWateringClicks"
)
@@ -99,4 +99,11 @@ class RainMachineUpdateEntity(RainMachineEntity, UpdateEntity):
UpdateStates.UPGRADING,
UpdateStates.REBOOT,
)
self._attr_latest_version = data["packageDetails"]["newVersion"]
# The RainMachine API docs say that multiple "packages" can be updated, but
# don't give details on what types exist (which makes it impossible to have
# update entities per update type); so, we use the first one (with the idea that
# after it succeeds, the entity will show the next update):
package_details = data["packageDetails"][0]
self._attr_latest_version = package_details["newVersion"]
self._attr_title = package_details["packageName"]
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Risco",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/risco",
"requirements": ["pyrisco==0.5.4"],
"requirements": ["pyrisco==0.5.5"],
"codeowners": ["@OnFreund"],
"quality_scale": "platinum",
"iot_class": "local_push",
@@ -458,7 +458,7 @@ class SamsungTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
)
if hostname := urlparse(discovery_info.ssdp_location or "").hostname:
self._host = hostname
self._manufacturer = discovery_info.upnp[ssdp.ATTR_UPNP_MANUFACTURER]
self._manufacturer = discovery_info.upnp.get(ssdp.ATTR_UPNP_MANUFACTURER)
self._abort_if_manufacturer_is_not_samsung()
# Set defaults, in case they cannot be extracted from device_info
+24 -1
View File
@@ -8,7 +8,7 @@ from typing import Any, cast
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components.blueprint import BlueprintInputs
from homeassistant.components.blueprint import CONF_USE_BLUEPRINT, BlueprintInputs
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_MODE,
@@ -18,6 +18,7 @@ from homeassistant.const import (
CONF_ICON,
CONF_MODE,
CONF_NAME,
CONF_PATH,
CONF_SEQUENCE,
CONF_VARIABLES,
SERVICE_RELOAD,
@@ -165,6 +166,21 @@ def areas_in_script(hass: HomeAssistant, entity_id: str) -> list[str]:
return list(script_entity.script.referenced_areas)
@callback
def scripts_with_blueprint(hass: HomeAssistant, blueprint_path: str) -> list[str]:
"""Return all scripts that reference the blueprint."""
if DOMAIN not in hass.data:
return []
component = hass.data[DOMAIN]
return [
script_entity.entity_id
for script_entity in component.entities
if script_entity.referenced_blueprint == blueprint_path
]
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Load the scripts from the configuration."""
hass.data[DOMAIN] = component = EntityComponent(LOGGER, DOMAIN, hass)
@@ -372,6 +388,13 @@ class ScriptEntity(ToggleEntity, RestoreEntity):
"""Return true if script is on."""
return self.script.is_running
@property
def referenced_blueprint(self):
"""Return referenced blueprint or None."""
if self._blueprint_inputs is None:
return None
return self._blueprint_inputs[CONF_USE_BLUEPRINT][CONF_PATH]
@callback
def async_change_listener(self):
"""Update state."""
+8 -1
View File
@@ -8,8 +8,15 @@ from .const import DOMAIN, LOGGER
DATA_BLUEPRINTS = "script_blueprints"
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
"""Return True if any script references the blueprint."""
from . import scripts_with_blueprint # pylint: disable=import-outside-toplevel
return len(scripts_with_blueprint(hass, blueprint_path)) > 0
@singleton(DATA_BLUEPRINTS)
@callback
def async_get_blueprints(hass: HomeAssistant) -> DomainBlueprints:
"""Get script blueprints."""
return DomainBlueprints(hass, DOMAIN, LOGGER)
return DomainBlueprints(hass, DOMAIN, LOGGER, _blueprint_in_use)
@@ -2,7 +2,7 @@
"domain": "sensibo",
"name": "Sensibo",
"documentation": "https://www.home-assistant.io/integrations/sensibo",
"requirements": ["pysensibo==1.0.19"],
"requirements": ["pysensibo==1.0.20"],
"config_flow": true,
"codeowners": ["@andrey-git", "@gjohansson-ST"],
"iot_class": "cloud_polling",
@@ -3,7 +3,7 @@
"name": "Sony Songpal",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/songpal",
"requirements": ["python-songpal==0.15"],
"requirements": ["python-songpal==0.15.1"],
"codeowners": ["@rytilahti", "@shenxn"],
"ssdp": [
{

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