Compare commits

..

217 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
Paulus Schoutsen c8ad8a6d86 Bumped version to 2022.9.0b6 2022-09-06 12:55:44 -04:00
Bram Kragten 9155f669e9 Update frontend to 20220906.0 (#77910) 2022-09-06 12:55:37 -04:00
J. Nick Koston e1e153f391 Bump bluetooth-auto-recovery to 0.3.1 (#77898) 2022-09-06 12:55:36 -04:00
Artem Draft 1dbcf88e15 Bump pybravia to 0.2.2 (#77867) 2022-09-06 12:55:35 -04:00
Raman Gupta a13438c5b0 Improve performance impact of zwave_js update entity and other tweaks (#77866)
* Improve performance impact of zwave_js update entity and other tweaks

* Reduce concurrent polls

* we need to write state after setting in progress to false

* Fix existing tests

* Fix tests by fixing fixtures

* remove redundant conditional

* Add test for delayed startup

* tweaks

* outdent happy path

* Add missing PROGRESS feature support

* Update homeassistant/components/zwave_js/update.py

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

* Update homeassistant/components/zwave_js/update.py

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

* Fix tests by reverting outdent, PR comments, mark callback

* Remove redundant conditional

* make more readable

* Remove unused SCAN_INTERVAL

* Catch FailedZWaveCommand

* Add comment and remove poll unsub on update

* Fix catching error and add test

* readability

* Fix tests

* Add assertions

* rely on built in progress indicator

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2022-09-06 12:55:35 -04:00
J. Nick Koston d98687b789 Bump thermopro-ble to 0.4.3 (#77863)
* Bump thermopro-ble to 0.4.2

- Turns on rounding of long values
- Uses bluetooth-data-tools under the hood
- Adds the TP393 since it works without any changes to the parser

Changelog: https://github.com/Bluetooth-Devices/thermopro-ble/compare/v0.4.0...v0.4.2

* bump again for device detection fix
2022-09-06 12:55:34 -04:00
Marc Mueller 319b0b8902 Pin astroid to fix pylint (#77862) 2022-09-06 12:55:33 -04:00
J. Nick Koston 62dcbc4d4a Add RSSI to the bluetooth debug log (#77860) 2022-09-06 12:55:33 -04:00
J. Nick Koston 6989b16274 Bump zeroconf to 0.39.1 (#77859) 2022-09-06 12:55:32 -04:00
J. Nick Koston 31d085cdf8 Fix history stats device class when type is not time (#77855) 2022-09-06 12:55:31 -04:00
Oliver Völker 61ee621c90 Adjust Renault default scan interval (#77823)
raise DEFAULT_SCAN_INTERVAL to 7 minutes

This PR is raising the default scan interval for the Renault API from 5 minutes to 7 minutes. Lower intervals fail sometimes, maybe due to quota limitations. This seems to be a working interval as described in home-assistant#73220
2022-09-06 12:55:30 -04:00
Yevhenii Vaskivskyi f5e61ecdec Handle exception on projector being unavailable (#77802) 2022-09-06 12:55:30 -04:00
G Johansson 2bfcdc66b6 Allow empty db in SQL options flow (#77777) 2022-09-06 12:55:29 -04:00
Martin Hjelmare 3240f8f938 Refactor zwave_js event handling (#77732)
* Refactor zwave_js event handling

* Clean up
2022-09-06 12:55:28 -04:00
Steven Looman 74ddc336ca Use identifiers host and serial number to match device (#75657) 2022-09-06 12:55:28 -04:00
Paulus Schoutsen 6c36d5acaa Bumped version to 2022.9.0b5 2022-09-05 14:28:36 -04:00
Bram Kragten e8c4711d88 Update frontend to 20220905.0 (#77854) 2022-09-05 14:28:26 -04:00
J. Nick Koston bca9dc1f61 Bump govee-ble to 0.17.2 (#77849) 2022-09-05 14:28:25 -04:00
J. Nick Koston 4f8421617e Bump led-ble to 0.7.0 (#77845) 2022-09-05 14:28:24 -04:00
Erik Montnemery 40421b41f7 Add the hardware integration to default_config (#77840) 2022-09-05 14:28:24 -04:00
Jc2k b0ff4fc057 Less verbose error logs for bleak connection errors in ActiveBluetoothProcessorCoordinator (#77839)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-09-05 14:28:23 -04:00
Charles Garwood 605e350159 Add remoteAdminPasswordEnd to redacted keys in fully_kiosk diagnostics (#77837)
Add remoteAdminPasswordEnd to redacted keys in diagnostics
2022-09-05 14:28:22 -04:00
Artem Draft ad8cd9c957 Bump pybravia to 0.2.1 (#77832) 2022-09-05 14:28:21 -04:00
Raman Gupta e8ab4eef44 Fix device info for zwave_js device entities (#77821) 2022-09-05 14:28:21 -04:00
J. Nick Koston b1241bf0f2 Fix isy994 calling sync api in async context (#77812) 2022-09-05 14:28:20 -04:00
J. Nick Koston f3e811417f Prefilter noisy apple devices from bluetooth (#77808) 2022-09-05 14:28:19 -04:00
Ernst Klamer 1231ba4d03 Rename BThome to BTHome (#77807)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-09-05 14:28:19 -04:00
G Johansson e07554dc25 Bump yale_smart_alarm_client to 0.3.9 (#77797) 2022-09-05 14:28:18 -04:00
Robert Hillis 2fa517b81b Make Sonos typing more complete (#68072) 2022-09-05 14:28:17 -04:00
Paulus Schoutsen 0d042d496d Bumped version to 2022.9.0b4 2022-09-04 13:00:37 -04:00
G Johansson c8156d5de6 Bump pysensibo to 1.0.19 (#77790) 2022-09-04 13:00:28 -04:00
J. Nick Koston 9f06baa778 Bump led-ble to 0.6.0 (#77788)
* Bump ble-led to 0.6.0

Fixes reading the white channel on same devices

Changelog: https://github.com/Bluetooth-Devices/led-ble/compare/v0.5.4...v0.6.0

* Bump flux_led to 0.28.32

Changelog: https://github.com/Danielhiversen/flux_led/compare/0.28.31...0.28.32

Fixes white channel support for some more older protocols

* keep them in sync

* Update homeassistant/components/led_ble/manifest.json
2022-09-04 13:00:27 -04:00
J. Nick Koston 52abf0851b Bump flux_led to 0.28.32 (#77787) 2022-09-04 13:00:27 -04:00
Justin Vanderhooft da83ceca5b Tweak unique id formatting for Melnor Bluetooth switches (#77773) 2022-09-04 13:00:26 -04:00
Avi Miller f9b95cc4a4 Fix lifx service call interference (#77770)
* Fix #77735 by restoring the wait to let state settle

Signed-off-by: Avi Miller <me@dje.li>

* Skip the asyncio.sleep during testing

Signed-off-by: Avi Miller <me@dje.li>

* Patch out asyncio.sleep for lifx tests

Signed-off-by: Avi Miller <me@dje.li>

* Patch out a constant instead of overriding asyncio.sleep directly

Signed-off-by: Avi Miller <me@dje.li>

Signed-off-by: Avi Miller <me@dje.li>
2022-09-04 13:00:25 -04:00
Avi Miller f60ae40661 Rename the binary sensor to better reflect its purpose (#77711) 2022-09-04 13:00:25 -04:00
Avi Miller ea0b406692 Add binary sensor platform to LIFX integration (#77535)
Co-authored-by: J. Nick Koston <nick@koston.org>
2022-09-04 13:00:24 -04:00
Michael 9387449abf Replace archived sucks by py-sucks and bump to 0.9.8 for Ecovacs integration (#77768) 2022-09-04 12:58:19 -04:00
Matt Zimmerman 5f4013164c Update smarttub to 0.0.33 (#77766) 2022-09-04 12:58:18 -04:00
Raman Gupta 3856178dc0 Handle dead nodes in zwave_js update entity (#77763) 2022-09-04 12:58:17 -04:00
J. Nick Koston 32a9fba58e Increase default august timeout (#77762)
Fixes
```
2022-08-28 20:32:46.223 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved
Traceback (most recent call last):
  File "/Users/bdraco/home-assistant/homeassistant/helpers/debounce.py", line 82, in async_call
    await task
  File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 49, in _async_update_house_id
    await self._async_update_house_id(house_id)
  File "/Users/bdraco/home-assistant/homeassistant/components/august/activity.py", line 137, in _async_update_house_id
    activities = await self._api.async_get_house_activities(
  File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 96, in async_get_house_activities
    response = await self._async_dict_to_api(
  File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/yalexs/api_async.py", line 294, in _async_dict_to_api
    response = await self._aiohttp_session.request(method, url, **api_dict)
  File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/client.py", line 466, in _request
    with timer:
  File "/Users/bdraco/home-assistant/venv/lib/python3.10/site-packages/aiohttp/helpers.py", line 721, in __exit__
    raise asyncio.TimeoutError from None
asyncio.exceptions.TimeoutError
```
2022-09-04 12:58:17 -04:00
J. Nick Koston 9733887b6a Add BlueMaestro integration (#77758)
* Add BlueMaestro integration

* tests

* dc
2022-09-04 12:58:16 -04:00
Michael b215514c90 Fix upgrade api disabling during setup of Synology DSM (#77753) 2022-09-04 12:58:15 -04:00
Pete 0e930fd626 Fix setting and reading percentage for MIOT based fans (#77626) 2022-09-04 12:58:15 -04:00
Simon Hansen cd4c31bc79 Convert platform in iss integration (#77218)
* Hopefully fix everthing and be happy

* ...

* update coverage file

* Fix tests
2022-09-04 12:58:14 -04:00
starkillerOG bc04755d05 Register xiaomi_miio unload callbacks later in setup (#76714) 2022-09-04 12:58:13 -04:00
Paulus Schoutsen 041eaf27a9 Bumped version to 2022.9.0b3 2022-09-02 20:54:37 -04:00
Paulus Schoutsen d6a99da461 Bump frontend to 20220902.0 (#77734) 2022-09-02 20:54:30 -04:00
Raman Gupta 1d2439a6e5 Change zwave_js firmware update service API key (#77719)
* Change zwave_js firmware update service API key

* Update const.py
2022-09-02 20:54:29 -04:00
J. Nick Koston 6fff633325 Bump bluetooth-adapters to 3.3.4 (#77705) 2022-09-02 20:54:29 -04:00
Nathan Spencer 9652c0c326 Adjust litterrobot platform loading/unloading (#77682)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2022-09-02 20:54:28 -04:00
Christopher Bailey 36c1b9a419 Fix timezone edge cases for Unifi Protect media source (#77636)
* Fixes timezone edge cases for Unifi Protect media source

* linting
2022-09-02 20:54:27 -04:00
Paulus Schoutsen a10a16ab21 Bumped version to 2022.9.0b2 2022-09-01 21:25:12 -04:00
Paulus Schoutsen 1f9c5ff369 Bump frontend to 20220901.0 (#77689) 2022-09-01 21:25:05 -04:00
J. Nick Koston f4273a098d Bump bluetooth-adapters to 0.3.3 (#77683) 2022-09-01 21:25:05 -04:00
Paulus Schoutsen 329c692065 Pin Pandas 1.4.3 (#77679) 2022-09-01 21:25:04 -04:00
J. Nick Koston dc2c0a159f Ensure unique id is set for esphome when setup via user flow (#77677) 2022-09-01 21:25:03 -04:00
J. Nick Koston c9d4924dea Bump pySwitchbot to 0.18.22 (#77673) 2022-09-01 21:25:02 -04:00
starkillerOG 0cdbb295bc bump pynetgear to 0.10.8 (#77672) 2022-09-01 21:25:02 -04:00
Jc2k ee0e12ac46 Fix async_all_discovered_devices(False) to return connectable and unconnectable devices (#77670) 2022-09-01 21:25:01 -04:00
Erik Montnemery 377791d6e7 Include entity registry id in entity registry WS API (#77668) 2022-09-01 21:25:00 -04:00
Erik Montnemery 37e425db30 Clean up user overridden device class in entity registry (#77662) 2022-09-01 21:25:00 -04:00
Kevin Stillhammer 073ca240f1 Required option_flow values for here_travel_time (#77651) 2022-09-01 21:24:59 -04:00
luar123 68a01562ec Add and remove Snapcast client/group callbacks properly (#77624)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2022-09-01 21:24:58 -04:00
On Freund 8c697b1881 Increase sleep in Risco setup (#77619) 2022-09-01 21:24:57 -04:00
Nathan Spencer d6b2f0ff76 Code quality improvements for litterrobot integration (#77605) 2022-09-01 21:24:57 -04:00
uvjustin b3830d0f17 Fix basic browse_media support in forked-daapd (#77595) 2022-09-01 21:24:56 -04:00
Kevin Stillhammer aa57594d21 Required config_flow values for here_travel_time (#75026) 2022-09-01 21:24:55 -04:00
377 changed files with 14425 additions and 8330 deletions
+2 -2
View File
@@ -587,7 +587,7 @@ omit =
homeassistant/components/iqvia/sensor.py
homeassistant/components/irish_rail_transport/sensor.py
homeassistant/components/iss/__init__.py
homeassistant/components/iss/binary_sensor.py
homeassistant/components/iss/sensor.py
homeassistant/components/isy994/__init__.py
homeassistant/components/isy994/binary_sensor.py
homeassistant/components/isy994/climate.py
@@ -1216,7 +1216,7 @@ omit =
homeassistant/components/switchbot/const.py
homeassistant/components/switchbot/entity.py
homeassistant/components/switchbot/cover.py
homeassistant/components/switchbot/light.py
homeassistant/components/switchbot/light.py
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/coordinator.py
homeassistant/components/switchmate/switch.py
+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:
+5 -3
View File
@@ -137,6 +137,8 @@ build.json @home-assistant/supervisor
/tests/components/blebox/ @bbx-a @riokuu
/homeassistant/components/blink/ @fronzbot
/tests/components/blink/ @fronzbot
/homeassistant/components/bluemaestro/ @bdraco
/tests/components/bluemaestro/ @bdraco
/homeassistant/components/blueprint/ @home-assistant/core
/tests/components/blueprint/ @home-assistant/core
/homeassistant/components/bluesound/ @thrawnarn
@@ -275,7 +277,7 @@ build.json @home-assistant/supervisor
/tests/components/ecobee/ @marthoc
/homeassistant/components/econet/ @vangorra @w1ll1am23
/tests/components/econet/ @vangorra @w1ll1am23
/homeassistant/components/ecovacs/ @OverloadUT
/homeassistant/components/ecovacs/ @OverloadUT @mib1185
/homeassistant/components/ecowitt/ @pvizeli
/tests/components/ecowitt/ @pvizeli
/homeassistant/components/edl21/ @mtdcr
@@ -865,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"
}
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ from datetime import timedelta
from homeassistant.const import Platform
DEFAULT_TIMEOUT = 15
DEFAULT_TIMEOUT = 25
CONF_ACCESS_TOKEN_CACHE_FILE = "access_token_cache_file"
CONF_LOGIN_METHOD = "login_method"
@@ -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": [
{
@@ -0,0 +1,49 @@
"""The BlueMaestro integration."""
from __future__ import annotations
import logging
from bluemaestro_ble import BlueMaestroBluetoothDeviceData
from homeassistant.components.bluetooth import BluetoothScanningMode
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothProcessorCoordinator,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BlueMaestro BLE device from a config entry."""
address = entry.unique_id
assert address is not None
data = BlueMaestroBluetoothDeviceData()
coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id
] = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=data.update,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
) # only start after all platforms have had a chance to subscribe
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
@@ -0,0 +1,94 @@
"""Config flow for bluemaestro ble integration."""
from __future__ import annotations
from typing import Any
from bluemaestro_ble import BlueMaestroBluetoothDeviceData as DeviceData
import voluptuous as vol
from homeassistant.components.bluetooth import (
BluetoothServiceInfoBleak,
async_discovered_service_info,
)
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_ADDRESS
from homeassistant.data_entry_flow import FlowResult
from .const import DOMAIN
class BlueMaestroConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for bluemaestro."""
VERSION = 1
def __init__(self) -> None:
"""Initialize the config flow."""
self._discovery_info: BluetoothServiceInfoBleak | None = None
self._discovered_device: DeviceData | None = None
self._discovered_devices: dict[str, str] = {}
async def async_step_bluetooth(
self, discovery_info: BluetoothServiceInfoBleak
) -> FlowResult:
"""Handle the bluetooth discovery step."""
await self.async_set_unique_id(discovery_info.address)
self._abort_if_unique_id_configured()
device = DeviceData()
if not device.supported(discovery_info):
return self.async_abort(reason="not_supported")
self._discovery_info = discovery_info
self._discovered_device = device
return await self.async_step_bluetooth_confirm()
async def async_step_bluetooth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Confirm discovery."""
assert self._discovered_device is not None
device = self._discovered_device
assert self._discovery_info is not None
discovery_info = self._discovery_info
title = device.title or device.get_device_name() or discovery_info.name
if user_input is not None:
return self.async_create_entry(title=title, data={})
self._set_confirm_only()
placeholders = {"name": title}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="bluetooth_confirm", description_placeholders=placeholders
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the user step to pick discovered device."""
if user_input is not None:
address = user_input[CONF_ADDRESS]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=self._discovered_devices[address], data={}
)
current_addresses = self._async_current_ids()
for discovery_info in async_discovered_service_info(self.hass, False):
address = discovery_info.address
if address in current_addresses or address in self._discovered_devices:
continue
device = DeviceData()
if device.supported(discovery_info):
self._discovered_devices[address] = (
device.title or device.get_device_name() or discovery_info.name
)
if not self._discovered_devices:
return self.async_abort(reason="no_devices_found")
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
),
)
@@ -0,0 +1,3 @@
"""Constants for the BlueMaestro integration."""
DOMAIN = "bluemaestro"
@@ -0,0 +1,31 @@
"""Support for BlueMaestro devices."""
from __future__ import annotations
from bluemaestro_ble import DeviceKey, SensorDeviceInfo
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothEntityKey,
)
from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME
from homeassistant.helpers.entity import DeviceInfo
def device_key_to_bluetooth_entity_key(
device_key: DeviceKey,
) -> PassiveBluetoothEntityKey:
"""Convert a device key to an entity key."""
return PassiveBluetoothEntityKey(device_key.key, device_key.device_id)
def sensor_device_info_to_hass(
sensor_device_info: SensorDeviceInfo,
) -> DeviceInfo:
"""Convert a bluemaestro device info to a sensor device info."""
hass_device_info = DeviceInfo({})
if sensor_device_info.name is not None:
hass_device_info[ATTR_NAME] = sensor_device_info.name
if sensor_device_info.manufacturer is not None:
hass_device_info[ATTR_MANUFACTURER] = sensor_device_info.manufacturer
if sensor_device_info.model is not None:
hass_device_info[ATTR_MODEL] = sensor_device_info.model
return hass_device_info
@@ -0,0 +1,16 @@
{
"domain": "bluemaestro",
"name": "BlueMaestro",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluemaestro",
"bluetooth": [
{
"manufacturer_id": 307,
"connectable": false
}
],
"requirements": ["bluemaestro-ble==0.2.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"iot_class": "local_push"
}
@@ -0,0 +1,149 @@
"""Support for BlueMaestro sensors."""
from __future__ import annotations
from typing import Optional, Union
from bluemaestro_ble import (
SensorDeviceClass as BlueMaestroSensorDeviceClass,
SensorUpdate,
Units,
)
from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
PERCENTAGE,
PRESSURE_MBAR,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .device import device_key_to_bluetooth_entity_key, sensor_device_info_to_hass
SENSOR_DESCRIPTIONS = {
(BlueMaestroSensorDeviceClass.BATTERY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.BATTERY}_{Units.PERCENTAGE}",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
(BlueMaestroSensorDeviceClass.HUMIDITY, Units.PERCENTAGE): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.HUMIDITY}_{Units.PERCENTAGE}",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
(
BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH,
Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.SIGNAL_STRENGTH}_{Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT}",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
(
BlueMaestroSensorDeviceClass.TEMPERATURE,
Units.TEMP_CELSIUS,
): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.TEMPERATURE}_{Units.TEMP_CELSIUS}",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
(
BlueMaestroSensorDeviceClass.DEW_POINT,
Units.TEMP_CELSIUS,
): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.DEW_POINT}_{Units.TEMP_CELSIUS}",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
),
(
BlueMaestroSensorDeviceClass.PRESSURE,
Units.PRESSURE_MBAR,
): SensorEntityDescription(
key=f"{BlueMaestroSensorDeviceClass.PRESSURE}_{Units.PRESSURE_MBAR}",
device_class=SensorDeviceClass.PRESSURE,
native_unit_of_measurement=PRESSURE_MBAR,
state_class=SensorStateClass.MEASUREMENT,
),
}
def sensor_update_to_bluetooth_data_update(
sensor_update: SensorUpdate,
) -> PassiveBluetoothDataUpdate:
"""Convert a sensor update to a bluetooth data update."""
return PassiveBluetoothDataUpdate(
devices={
device_id: sensor_device_info_to_hass(device_info)
for device_id, device_info in sensor_update.devices.items()
},
entity_descriptions={
device_key_to_bluetooth_entity_key(device_key): SENSOR_DESCRIPTIONS[
(description.device_class, description.native_unit_of_measurement)
]
for device_key, description in sensor_update.entity_descriptions.items()
if description.device_class and description.native_unit_of_measurement
},
entity_data={
device_key_to_bluetooth_entity_key(device_key): sensor_values.native_value
for device_key, sensor_values in sensor_update.entity_values.items()
},
entity_names={
device_key_to_bluetooth_entity_key(device_key): sensor_values.name
for device_key, sensor_values in sensor_update.entity_values.items()
},
)
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BlueMaestro BLE sensors."""
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id
]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
BlueMaestroBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
class BlueMaestroBluetoothSensorEntity(
PassiveBluetoothProcessorEntity[
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
],
SensorEntity,
):
"""Representation of a BlueMaestro sensor."""
@property
def native_value(self) -> int | float | None:
"""Return the native value."""
return self.processor.entity_data.get(self.entity_key)
@@ -0,0 +1,22 @@
{
"config": {
"flow_title": "[%key:component::bluetooth::config::flow_title%]",
"step": {
"user": {
"description": "[%key:component::bluetooth::config::step::user::description%]",
"data": {
"address": "[%key:component::bluetooth::config::step::user::data::address%]"
}
},
"bluetooth_confirm": {
"description": "[%key:component::bluetooth::config::step::bluetooth_confirm::description%]"
}
},
"abort": {
"not_supported": "Device not supported",
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
}
}
@@ -0,0 +1,22 @@
{
"config": {
"abort": {
"already_configured": "Device is already configured",
"already_in_progress": "Configuration flow is already in progress",
"no_devices_found": "No devices found on the network",
"not_supported": "Device not supported"
},
"flow_title": "{name}",
"step": {
"bluetooth_confirm": {
"description": "Do you want to setup {name}?"
},
"user": {
"data": {
"address": "Device"
},
"description": "Choose a device to setup"
}
}
}
}
@@ -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
@@ -6,6 +6,8 @@ import logging
import time
from typing import Any, Generic, TypeVar
from bleak import BleakError
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.debounce import Debouncer
@@ -109,6 +111,13 @@ class ActiveBluetoothProcessorCoordinator(
try:
update = await self._async_poll_data(self._last_service_info)
except BleakError as exc:
if self.last_poll_successful:
self.logger.error(
"%s: Bluetooth error whilst polling: %s", self.address, str(exc)
)
self.last_poll_successful = False
return
except Exception: # pylint: disable=broad-except
if self.last_poll_successful:
self.logger.exception("%s: Failure while polling", self.address)
+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
+30 -8
View File
@@ -54,8 +54,13 @@ if TYPE_CHECKING:
FILTER_UUIDS: Final = "UUIDs"
APPLE_MFR_ID: Final = 76
APPLE_HOMEKIT_START_BYTE: Final = 0x06 # homekit_controller
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__)
@@ -79,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(
@@ -221,10 +226,14 @@ class BluetoothManager:
@hass_callback
def async_all_discovered_devices(self, connectable: bool) -> Iterable[BLEDevice]:
"""Return all of discovered devices from all the scanners including duplicates."""
return itertools.chain.from_iterable(
scanner.discovered_devices
for scanner in self._get_scanners_by_type(connectable)
yield from itertools.chain.from_iterable(
scanner.discovered_devices for scanner in self._get_scanners_by_type(True)
)
if not connectable:
yield from itertools.chain.from_iterable(
scanner.discovered_devices
for scanner in self._get_scanners_by_type(False)
)
@hass_callback
def async_discovered_devices(self, connectable: bool) -> list[BLEDevice]:
@@ -286,6 +295,19 @@ class BluetoothManager:
than the source from the history or the timestamp
in the history is older than 180s
"""
# Pre-filter noisy apple devices as they can account for 20-35% of the
# traffic on a typical network.
advertisement_data = service_info.advertisement
manufacturer_data = advertisement_data.manufacturer_data
if (
len(manufacturer_data) == 1
and (apple_data := manufacturer_data.get(APPLE_MFR_ID))
and apple_data[0] not in APPLE_START_BYTES_WANTED
and not advertisement_data.service_data
):
return
device = service_info.device
connectable = service_info.connectable
address = device.address
@@ -295,7 +317,6 @@ class BluetoothManager:
return
self._history[address] = service_info
advertisement_data = service_info.advertisement
source = service_info.source
if connectable:
@@ -307,12 +328,13 @@ class BluetoothManager:
matched_domains = self._integration_matcher.match_domains(service_info)
_LOGGER.debug(
"%s: %s %s connectable: %s match: %s",
"%s: %s %s connectable: %s match: %s rssi: %s",
source,
address,
advertisement_data,
connectable,
matched_domains,
device.rssi,
)
for match in self._callback_index.match_callbacks(service_info):
@@ -363,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.2",
"bluetooth-auto-recovery==0.3.0"
"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
@@ -7,7 +7,13 @@ from functools import wraps
import logging
from typing import Any, Final, TypeVar
from pybravia import BraviaTV, BraviaTVError, BraviaTVNotFound
from pybravia import (
BraviaTV,
BraviaTVConnectionError,
BraviaTVConnectionTimeout,
BraviaTVError,
BraviaTVNotFound,
)
from typing_extensions import Concatenate, ParamSpec
from homeassistant.components.media_player.const import (
@@ -130,6 +136,10 @@ class BraviaTVCoordinator(DataUpdateCoordinator[None]):
_LOGGER.debug("Update skipped, Bravia API service is reloading")
return
raise UpdateFailed("Error communicating with device") from err
except (BraviaTVConnectionError, BraviaTVConnectionTimeout):
self.is_on = False
self.connected = False
_LOGGER.debug("Update skipped, Bravia TV is off")
except BraviaTVError as err:
self.is_on = False
self.connected = False
@@ -2,7 +2,7 @@
"domain": "braviatv",
"name": "Sony Bravia TV",
"documentation": "https://www.home-assistant.io/integrations/braviatv",
"requirements": ["pybravia==0.2.0"],
"requirements": ["pybravia==0.2.2"],
"codeowners": ["@bieniu", "@Drafteed"],
"config_flow": true,
"iot_class": "local_polling",
@@ -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"]
+5 -5
View File
@@ -1,9 +1,9 @@
"""The BThome Bluetooth integration."""
"""The BTHome Bluetooth integration."""
from __future__ import annotations
import logging
from bthome_ble import BThomeBluetoothDeviceData, SensorUpdate
from bthome_ble import BTHomeBluetoothDeviceData, SensorUpdate
from bthome_ble.parser import EncryptionScheme
from homeassistant.components.bluetooth import (
@@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__)
def process_service_info(
hass: HomeAssistant,
entry: ConfigEntry,
data: BThomeBluetoothDeviceData,
data: BTHomeBluetoothDeviceData,
service_info: BluetoothServiceInfoBleak,
) -> SensorUpdate:
"""Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
@@ -40,14 +40,14 @@ def process_service_info(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BThome Bluetooth from a config entry."""
"""Set up BTHome Bluetooth from a config entry."""
address = entry.unique_id
assert address is not None
kwargs = {}
if bindkey := entry.data.get("bindkey"):
kwargs["bindkey"] = bytes.fromhex(bindkey)
data = BThomeBluetoothDeviceData(**kwargs)
data = BTHomeBluetoothDeviceData(**kwargs)
coordinator = hass.data.setdefault(DOMAIN, {})[
entry.entry_id
@@ -1,11 +1,11 @@
"""Config flow for BThome Bluetooth integration."""
"""Config flow for BTHome Bluetooth integration."""
from __future__ import annotations
from collections.abc import Mapping
import dataclasses
from typing import Any
from bthome_ble import BThomeBluetoothDeviceData as DeviceData
from bthome_ble import BTHomeBluetoothDeviceData as DeviceData
from bthome_ble.parser import EncryptionScheme
import voluptuous as vol
@@ -34,8 +34,8 @@ def _title(discovery_info: BluetoothServiceInfo, device: DeviceData) -> str:
return device.title or device.get_device_name() or discovery_info.name
class BThomeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BThome Bluetooth."""
class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for BTHome Bluetooth."""
VERSION = 1
@@ -68,7 +68,7 @@ class BThomeConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_get_encryption_key(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Enter a bindkey for an encrypted BThome device."""
"""Enter a bindkey for an encrypted BTHome device."""
assert self._discovery_info
assert self._discovered_device
+1 -1
View File
@@ -1,3 +1,3 @@
"""Constants for the BThome Bluetooth integration."""
"""Constants for the BTHome Bluetooth integration."""
DOMAIN = "bthome"
+1 -1
View File
@@ -1,4 +1,4 @@
"""Support for BThome Bluetooth devices."""
"""Support for BTHome Bluetooth devices."""
from __future__ import annotations
from bthome_ble import DeviceKey, SensorDeviceInfo
@@ -1,6 +1,6 @@
{
"domain": "bthome",
"name": "BThome",
"name": "BTHome",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bthome",
"bluetooth": [
@@ -13,7 +13,7 @@
"service_data_uuid": "0000181e-0000-1000-8000-00805f9b34fb"
}
],
"requirements": ["bthome-ble==0.5.2"],
"requirements": ["bthome-ble==1.0.0"],
"dependencies": ["bluetooth"],
"codeowners": ["@Ernst79"],
"iot_class": "local_push"
+5 -5
View File
@@ -1,4 +1,4 @@
"""Support for BThome sensors."""
"""Support for BTHome sensors."""
from __future__ import annotations
from typing import Optional, Union
@@ -202,26 +202,26 @@ async def async_setup_entry(
entry: config_entries.ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the BThome BLE sensors."""
"""Set up the BTHome BLE sensors."""
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id
]
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(
BThomeBluetoothSensorEntity, async_add_entities
BTHomeBluetoothSensorEntity, async_add_entities
)
)
entry.async_on_unload(coordinator.async_register_processor(processor))
class BThomeBluetoothSensorEntity(
class BTHomeBluetoothSensorEntity(
PassiveBluetoothProcessorEntity[
PassiveBluetoothDataProcessor[Optional[Union[float, int]]]
],
SensorEntity,
):
"""Representation of a BThome BLE sensor."""
"""Representation of a BTHome BLE sensor."""
@property
def native_value(self) -> int | float | None:
@@ -237,6 +237,7 @@ def _entry_dict(entry: er.RegistryEntry) -> dict[str, Any]:
"entity_id": entry.entity_id,
"hidden_by": entry.hidden_by,
"icon": entry.icon,
"id": entry.id,
"name": entry.name,
"original_name": entry.original_name,
"platform": entry.platform,
+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(
@@ -11,8 +11,9 @@
"dhcp",
"energy",
"frontend",
"homeassistant_alerts",
"hardware",
"history",
"homeassistant_alerts",
"input_boolean",
"input_button",
"input_datetime",
+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
@@ -2,8 +2,8 @@
"domain": "ecovacs",
"name": "Ecovacs",
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"requirements": ["sucks==0.9.4"],
"codeowners": ["@OverloadUT"],
"requirements": ["py-sucks==0.9.8"],
"codeowners": ["@OverloadUT", "@mib1185"],
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks"]
}
@@ -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__)
+1 -1
View File
@@ -3,7 +3,7 @@
"name": "Epson",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/epson",
"requirements": ["epson-projector==0.4.6"],
"requirements": ["epson-projector==0.5.0"],
"codeowners": ["@pszafer"],
"iot_class": "local_polling",
"loggers": ["epson_projector"]
@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from epson_projector import Projector
from epson_projector import Projector, ProjectorUnavailableError
from epson_projector.const import (
BACK,
BUSY,
@@ -20,7 +20,6 @@ from epson_projector.const import (
POWER,
SOURCE,
SOURCE_LIST,
STATE_UNAVAILABLE as EPSON_STATE_UNAVAILABLE,
TURN_OFF,
TURN_ON,
VOL_DOWN,
@@ -123,11 +122,16 @@ class EpsonProjectorMediaPlayer(MediaPlayerEntity):
async def async_update(self) -> None:
"""Update state of device."""
power_state = await self._projector.get_power()
_LOGGER.debug("Projector status: %s", power_state)
if not power_state or power_state == EPSON_STATE_UNAVAILABLE:
try:
power_state = await self._projector.get_power()
except ProjectorUnavailableError as ex:
_LOGGER.debug("Projector is unavailable: %s", ex)
self._attr_available = False
return
if not power_state:
self._attr_available = False
return
_LOGGER.debug("Projector status: %s", power_state)
self._attr_available = True
if power_state == EPSON_CODES[POWER]:
self._attr_state = STATE_ON
@@ -333,6 +333,10 @@ async def async_setup_entry( # noqa: C901
if entry_data.device_info is not None and entry_data.device_info.name:
cli.expected_name = entry_data.device_info.name
reconnect_logic.name = entry_data.device_info.name
if entry.unique_id is None:
hass.config_entries.async_update_entry(
entry, unique_id=entry_data.device_info.name
)
await reconnect_logic.start()
entry_data.cleanup_callbacks.append(reconnect_logic.stop_callback)
@@ -331,6 +331,8 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
await cli.disconnect(force=True)
self._name = self._device_info.name
await self.async_set_unique_id(self._name, raise_on_progress=False)
self._abort_if_unique_id_configured(updates={CONF_HOST: self._host})
return None
@@ -4,7 +4,7 @@
"config_flow": true,
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/flux_led",
"requirements": ["flux_led==0.28.31"],
"requirements": ["flux_led==0.28.32"],
"quality_scale": "platinum",
"codeowners": ["@icemanch", "@bdraco"],
"iot_class": "local_push",
@@ -1,6 +1,17 @@
"""Const for forked-daapd."""
from homeassistant.components.media_player import MediaPlayerEntityFeature
CAN_PLAY_TYPE = {
"audio/mp4",
"audio/aac",
"audio/mpeg",
"audio/flac",
"audio/ogg",
"audio/x-ms-wma",
"audio/aiff",
"audio/wav",
}
CALLBACK_TIMEOUT = 8 # max time between command and callback from forked-daapd server
CONF_LIBRESPOT_JAVA_PORT = "librespot_java_port"
CONF_MAX_PLAYLISTS = "max_playlists"
@@ -1,4 +1,6 @@
"""This library brings support for forked_daapd to Home Assistant."""
from __future__ import annotations
import asyncio
from collections import defaultdict
import logging
@@ -8,7 +10,7 @@ from pyforked_daapd import ForkedDaapdAPI
from pylibrespot_java import LibrespotJavaAPI
from homeassistant.components import media_source
from homeassistant.components.media_player import MediaPlayerEntity
from homeassistant.components.media_player import BrowseMedia, MediaPlayerEntity
from homeassistant.components.media_player.browse_media import (
async_process_play_media_url,
)
@@ -35,6 +37,7 @@ from homeassistant.util.dt import utcnow
from .const import (
CALLBACK_TIMEOUT,
CAN_PLAY_TYPE,
CONF_LIBRESPOT_JAVA_PORT,
CONF_MAX_PLAYLISTS,
CONF_TTS_PAUSE_TIME,
@@ -769,6 +772,18 @@ class ForkedDaapdMaster(MediaPlayerEntity):
)()
_LOGGER.warning("No pipe control available for %s", pipe_name)
async def async_browse_media(
self,
media_content_type: str | None = None,
media_content_id: str | None = None,
) -> BrowseMedia:
"""Implement the websocket media browsing helper."""
return await media_source.async_browse_media(
self.hass,
media_content_id,
content_filter=lambda bm: bm.media_content_type in CAN_PLAY_TYPE,
)
class ForkedDaapdUpdater:
"""Manage updates for the forked-daapd device."""
@@ -885,11 +900,3 @@ class ForkedDaapdUpdater:
self._api,
outputs_to_add,
)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
return await media_source.async_browse_media(
self.hass,
media_content_id,
content_filter=lambda item: item.media_content_type.startswith("audio/"),
)
@@ -2,7 +2,7 @@
"domain": "frontend",
"name": "Home Assistant Frontend",
"documentation": "https://www.home-assistant.io/integrations/frontend",
"requirements": ["home-assistant-frontend==20220831.0"],
"requirements": ["home-assistant-frontend==20220907.2"],
"dependencies": [
"api",
"auth",
@@ -51,6 +51,7 @@ SETTINGS_TO_REDACT = {
"sebExamKey",
"sebConfigKey",
"kioskPinEnc",
"remoteAdminPasswordEnc",
}
@@ -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.1"],
"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()
@@ -11,6 +11,8 @@ from homeassistant import config_entries
from homeassistant.const import (
CONF_API_KEY,
CONF_ENTITY_NAMESPACE,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_MODE,
CONF_NAME,
CONF_UNIT_SYSTEM,
@@ -22,7 +24,6 @@ from homeassistant.helpers.selector import (
EntitySelector,
LocationSelector,
TimeSelector,
selector,
)
from .const import (
@@ -30,6 +31,8 @@ from .const import (
CONF_ARRIVAL_TIME,
CONF_DEPARTURE,
CONF_DEPARTURE_TIME,
CONF_DESTINATION,
CONF_ORIGIN,
CONF_ROUTE_MODE,
CONF_TRAFFIC_MODE,
DEFAULT_NAME,
@@ -187,13 +190,25 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Configure origin by using gps coordinates."""
if user_input is not None:
self._config[CONF_ORIGIN_LATITUDE] = user_input["origin"]["latitude"]
self._config[CONF_ORIGIN_LONGITUDE] = user_input["origin"]["longitude"]
self._config[CONF_ORIGIN_LATITUDE] = user_input[CONF_ORIGIN][CONF_LATITUDE]
self._config[CONF_ORIGIN_LONGITUDE] = user_input[CONF_ORIGIN][
CONF_LONGITUDE
]
return self.async_show_menu(
step_id="destination_menu",
menu_options=["destination_coordinates", "destination_entity"],
)
schema = vol.Schema({"origin": selector({LocationSelector.selector_type: {}})})
schema = vol.Schema(
{
vol.Required(
CONF_ORIGIN,
default={
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
},
): LocationSelector()
}
)
return self.async_show_form(step_id="origin_coordinates", data_schema=schema)
async def async_step_origin_entity(
@@ -206,9 +221,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="destination_menu",
menu_options=["destination_coordinates", "destination_entity"],
)
schema = vol.Schema(
{CONF_ORIGIN_ENTITY_ID: selector({EntitySelector.selector_type: {}})}
)
schema = vol.Schema({vol.Required(CONF_ORIGIN_ENTITY_ID): EntitySelector()})
return self.async_show_form(step_id="origin_entity", data_schema=schema)
async def async_step_destination_coordinates(
@@ -217,11 +230,11 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Configure destination by using gps coordinates."""
if user_input is not None:
self._config[CONF_DESTINATION_LATITUDE] = user_input["destination"][
"latitude"
self._config[CONF_DESTINATION_LATITUDE] = user_input[CONF_DESTINATION][
CONF_LATITUDE
]
self._config[CONF_DESTINATION_LONGITUDE] = user_input["destination"][
"longitude"
self._config[CONF_DESTINATION_LONGITUDE] = user_input[CONF_DESTINATION][
CONF_LONGITUDE
]
return self.async_create_entry(
title=self._config[CONF_NAME],
@@ -229,7 +242,15 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
options=default_options(self.hass),
)
schema = vol.Schema(
{"destination": selector({LocationSelector.selector_type: {}})}
{
vol.Required(
CONF_DESTINATION,
default={
CONF_LATITUDE: self.hass.config.latitude,
CONF_LONGITUDE: self.hass.config.longitude,
},
): LocationSelector()
}
)
return self.async_show_form(
step_id="destination_coordinates", data_schema=schema
@@ -250,7 +271,7 @@ class HERETravelTimeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
options=default_options(self.hass),
)
schema = vol.Schema(
{CONF_DESTINATION_ENTITY_ID: selector({EntitySelector.selector_type: {}})}
{vol.Required(CONF_DESTINATION_ENTITY_ID): EntitySelector()}
)
return self.async_show_form(step_id="destination_entity", data_schema=schema)
@@ -339,28 +360,30 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow):
menu_options=["departure_time", "no_time"],
)
options = {
vol.Optional(
CONF_TRAFFIC_MODE,
default=self.config_entry.options.get(
CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED
),
): vol.In(TRAFFIC_MODES),
vol.Optional(
CONF_ROUTE_MODE,
default=self.config_entry.options.get(
CONF_ROUTE_MODE, ROUTE_MODE_FASTEST
),
): vol.In(ROUTE_MODES),
vol.Optional(
CONF_UNIT_SYSTEM,
default=self.config_entry.options.get(
CONF_UNIT_SYSTEM, self.hass.config.units.name
),
): vol.In(UNITS),
}
schema = vol.Schema(
{
vol.Optional(
CONF_TRAFFIC_MODE,
default=self.config_entry.options.get(
CONF_TRAFFIC_MODE, TRAFFIC_MODE_ENABLED
),
): vol.In(TRAFFIC_MODES),
vol.Optional(
CONF_ROUTE_MODE,
default=self.config_entry.options.get(
CONF_ROUTE_MODE, ROUTE_MODE_FASTEST
),
): vol.In(ROUTE_MODES),
vol.Optional(
CONF_UNIT_SYSTEM,
default=self.config_entry.options.get(
CONF_UNIT_SYSTEM, self.hass.config.units.name
),
): vol.In(UNITS),
}
)
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
return self.async_show_form(step_id="init", data_schema=schema)
async def async_step_no_time(
self, user_input: dict[str, Any] | None = None
@@ -376,12 +399,12 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow):
self._config[CONF_ARRIVAL_TIME] = user_input[CONF_ARRIVAL_TIME]
return self.async_create_entry(title="", data=self._config)
options = {"arrival_time": selector({TimeSelector.selector_type: {}})}
return self.async_show_form(
step_id="arrival_time", data_schema=vol.Schema(options)
schema = vol.Schema(
{vol.Required(CONF_ARRIVAL_TIME, default="00:00:00"): TimeSelector()}
)
return self.async_show_form(step_id="arrival_time", data_schema=schema)
async def async_step_departure_time(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
@@ -390,8 +413,8 @@ class HERETravelTimeOptionsFlow(config_entries.OptionsFlow):
self._config[CONF_DEPARTURE_TIME] = user_input[CONF_DEPARTURE_TIME]
return self.async_create_entry(title="", data=self._config)
options = {"departure_time": selector({TimeSelector.selector_type: {}})}
return self.async_show_form(
step_id="departure_time", data_schema=vol.Schema(options)
schema = vol.Schema(
{vol.Required(CONF_DEPARTURE_TIME, default="00:00:00"): TimeSelector()}
)
return self.async_show_form(step_id="departure_time", data_schema=schema)
@@ -9,9 +9,11 @@ DOMAIN = "here_travel_time"
DEFAULT_SCAN_INTERVAL = 300
CONF_DESTINATION = "destination"
CONF_DESTINATION_LATITUDE = "destination_latitude"
CONF_DESTINATION_LONGITUDE = "destination_longitude"
CONF_DESTINATION_ENTITY_ID = "destination_entity_id"
CONF_ORIGIN = "origin"
CONF_ORIGIN_LATITUDE = "origin_latitude"
CONF_ORIGIN_LONGITUDE = "origin_longitude"
CONF_ORIGIN_ENTITY_ID = "origin_entity_id"
@@ -143,7 +143,6 @@ class HistoryStatsSensorBase(
class HistoryStatsSensor(HistoryStatsSensorBase):
"""A HistoryStats sensor."""
_attr_device_class = SensorDeviceClass.DURATION
_attr_state_class = SensorStateClass.MEASUREMENT
def __init__(
@@ -157,6 +156,8 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
self._attr_native_unit_of_measurement = UNITS[sensor_type]
self._type = sensor_type
self._process_update()
if self._type == CONF_TYPE_TIME:
self._attr_device_class = SensorDeviceClass.DURATION
@callback
def _process_update(self) -> None:
@@ -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."""
+4 -10
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
import logging
import pyiss
@@ -18,7 +18,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR]
PLATFORMS = [Platform.SENSOR]
@dataclass
@@ -27,31 +27,25 @@ class IssData:
number_of_people_in_space: int
current_location: dict[str, str]
is_above: bool
next_rise: datetime
def update(iss: pyiss.ISS, latitude: float, longitude: float) -> IssData:
def update(iss: pyiss.ISS) -> IssData:
"""Retrieve data from the pyiss API."""
return IssData(
number_of_people_in_space=iss.number_of_people_in_space(),
current_location=iss.current_location(),
is_above=iss.is_ISS_above(latitude, longitude),
next_rise=iss.next_rise(latitude, longitude),
)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up this integration using UI."""
hass.data.setdefault(DOMAIN, {})
latitude = hass.config.latitude
longitude = hass.config.longitude
iss = pyiss.ISS()
async def async_update() -> IssData:
try:
return await hass.async_add_executor_job(update, iss, latitude, longitude)
return await hass.async_add_executor_job(update, iss)
except (HTTPError, requests.exceptions.ConnectionError) as ex:
raise UpdateFailed("Unable to retrieve data") from ex
+2 -5
View File
@@ -7,9 +7,10 @@ from homeassistant.const import CONF_NAME, CONF_SHOW_ON_MAP
from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult
from .binary_sensor import DEFAULT_NAME
from .const import DOMAIN
DEFAULT_NAME = "ISS"
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Config flow for iss component."""
@@ -30,10 +31,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
# Check if location have been defined.
if not self.hass.config.latitude and not self.hass.config.longitude:
return self.async_abort(reason="latitude_longitude_not_defined")
if user_input is not None:
return self.async_create_entry(
title=user_input.get(CONF_NAME, DEFAULT_NAME),
@@ -1,10 +1,10 @@
"""Support for iss binary sensor."""
"""Support for iss sensor."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP
from homeassistant.core import HomeAssistant
@@ -19,12 +19,6 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
ATTR_ISS_NEXT_RISE = "next_rise"
ATTR_ISS_NUMBER_PEOPLE_SPACE = "number_of_people_in_space"
DEFAULT_NAME = "ISS"
DEFAULT_DEVICE_CLASS = "visible"
async def async_setup_entry(
hass: HomeAssistant,
@@ -37,15 +31,11 @@ async def async_setup_entry(
name = entry.title
show_on_map = entry.options.get(CONF_SHOW_ON_MAP, False)
async_add_entities([IssBinarySensor(coordinator, name, show_on_map)])
async_add_entities([IssSensor(coordinator, name, show_on_map)])
class IssBinarySensor(
CoordinatorEntity[DataUpdateCoordinator[IssData]], BinarySensorEntity
):
"""Implementation of the ISS binary sensor."""
_attr_device_class = DEFAULT_DEVICE_CLASS
class IssSensor(CoordinatorEntity[DataUpdateCoordinator[IssData]], SensorEntity):
"""Implementation of the ISS sensor."""
def __init__(
self, coordinator: DataUpdateCoordinator[IssData], name: str, show: bool
@@ -57,17 +47,14 @@ class IssBinarySensor(
self._show_on_map = show
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return self.coordinator.data.is_above is True
def native_value(self) -> int:
"""Return number of people in space."""
return self.coordinator.data.number_of_people_in_space
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
attrs = {
ATTR_ISS_NUMBER_PEOPLE_SPACE: self.coordinator.data.number_of_people_in_space,
ATTR_ISS_NEXT_RISE: self.coordinator.data.next_rise,
}
attrs = {}
if self._show_on_map:
attrs[ATTR_LONGITUDE] = self.coordinator.data.current_location.get(
"longitude"
+1 -1
View File
@@ -75,7 +75,7 @@ class ISYEntity(Entity):
# New state attributes may be available, update the state.
self.async_write_ha_state()
self.hass.bus.fire("isy994_control", event_data)
self.hass.bus.async_fire("isy994_control", event_data)
@property
def device_info(self) -> DeviceInfo | None:
@@ -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.5.4"],
"requirements": ["led-ble==0.10.1"],
"dependencies": ["bluetooth"],
"codeowners": ["@bdraco"],
"bluetooth": [
@@ -11,7 +11,10 @@
{ "local_name": "BLE-LED*" },
{ "local_name": "LEDBLE*" },
{ "local_name": "Triones*" },
{ "local_name": "LEDBlue*" }
{ "local_name": "LEDBlue*" },
{ "local_name": "Dream~*" },
{ "local_name": "QHM-*" },
{ "local_name": "AP-*" }
],
"iot_class": "local_polling"
}
@@ -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
+1 -1
View File
@@ -57,7 +57,7 @@ CONFIG_SCHEMA = vol.All(
)
PLATFORMS = [Platform.BUTTON, Platform.LIGHT]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.LIGHT]
DISCOVERY_INTERVAL = timedelta(minutes=15)
MIGRATION_INTERVAL = timedelta(minutes=5)
@@ -0,0 +1,70 @@
"""Binary sensor entities for LIFX integration."""
from __future__ import annotations
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, HEV_CYCLE_STATE
from .coordinator import LIFXUpdateCoordinator
from .entity import LIFXEntity
from .util import lifx_features
HEV_CYCLE_STATE_SENSOR = BinarySensorEntityDescription(
key=HEV_CYCLE_STATE,
name="Clean Cycle",
entity_category=EntityCategory.DIAGNOSTIC,
device_class=BinarySensorDeviceClass.RUNNING,
)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up LIFX from a config entry."""
coordinator: LIFXUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if lifx_features(coordinator.device)["hev"]:
async_add_entities(
[
LIFXHevCycleBinarySensorEntity(
coordinator=coordinator, description=HEV_CYCLE_STATE_SENSOR
)
]
)
class LIFXHevCycleBinarySensorEntity(LIFXEntity, BinarySensorEntity):
"""LIFX HEV cycle state binary sensor."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: LIFXUpdateCoordinator,
description: BinarySensorEntityDescription,
) -> None:
"""Initialise the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_name = description.name
self._attr_unique_id = f"{coordinator.serial_number}_{description.key}"
self._async_update_attrs()
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
self._async_update_attrs()
super()._handle_coordinator_update()
@callback
def _async_update_attrs(self) -> None:
"""Handle coordinator updates."""
self._attr_is_on = self.coordinator.async_get_hev_cycle_state()
+10 -1
View File
@@ -29,6 +29,15 @@ IDENTIFY_WAVEFORM = {
IDENTIFY = "identify"
RESTART = "restart"
ATTR_DURATION = "duration"
ATTR_INDICATION = "indication"
ATTR_INFRARED = "infrared"
ATTR_POWER = "power"
ATTR_REMAINING = "remaining"
ATTR_ZONES = "zones"
HEV_CYCLE_STATE = "hev_cycle_state"
DATA_LIFX_MANAGER = "lifx_manager"
_LOGGER = logging.getLogger(__name__)
_LOGGER = logging.getLogger(__package__)
+25 -13
View File
@@ -15,6 +15,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import (
_LOGGER,
ATTR_REMAINING,
IDENTIFY_WAVEFORM,
MESSAGE_RETRIES,
MESSAGE_TIMEOUT,
@@ -24,6 +25,7 @@ from .const import (
from .util import async_execute_lifx, get_real_mac_addr, lifx_features
REQUEST_REFRESH_DELAY = 0.35
LIFX_IDENTIFY_DELAY = 3.0
class LIFXUpdateCoordinator(DataUpdateCoordinator):
@@ -91,7 +93,7 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
# Turn the bulb on first, flash for 3 seconds, then turn off
await self.async_set_power(state=True, duration=1)
await self.async_set_waveform_optional(value=IDENTIFY_WAVEFORM)
await asyncio.sleep(3)
await asyncio.sleep(LIFX_IDENTIFY_DELAY)
await self.async_set_power(state=False, duration=1)
async def _async_update_data(self) -> None:
@@ -101,26 +103,25 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
self.device.get_hostfirmware()
if self.device.product is None:
self.device.get_version()
try:
response = await async_execute_lifx(self.device.get_color)
except asyncio.TimeoutError as ex:
raise UpdateFailed(
f"Failed to fetch state from device: {self.device.ip_addr}"
) from ex
response = await async_execute_lifx(self.device.get_color)
if self.device.product is None:
raise UpdateFailed(
f"Failed to fetch get version from device: {self.device.ip_addr}"
)
# device.mac_addr is not the mac_address, its the serial number
if self.device.mac_addr == TARGET_ANY:
self.device.mac_addr = response.target_addr
if lifx_features(self.device)["multizone"]:
try:
await self.async_update_color_zones()
except asyncio.TimeoutError as ex:
raise UpdateFailed(
f"Failed to fetch zones from device: {self.device.ip_addr}"
) from ex
await self.async_update_color_zones()
if lifx_features(self.device)["hev"]:
if self.device.hev_cycle_configuration is None:
self.device.get_hev_configuration()
await self.async_get_hev_cycle()
async def async_update_color_zones(self) -> None:
"""Get updated color information for each zone."""
@@ -138,6 +139,17 @@ class LIFXUpdateCoordinator(DataUpdateCoordinator):
if zone == top - 1:
zone -= 1
def async_get_hev_cycle_state(self) -> bool | None:
"""Return the current HEV cycle state."""
if self.device.hev_cycle is None:
return None
return bool(self.device.hev_cycle.get(ATTR_REMAINING, 0) > 0)
async def async_get_hev_cycle(self) -> None:
"""Update the HEV cycle status from a LIFX Clean bulb."""
if lifx_features(self.device)["hev"]:
await async_execute_lifx(self.device.get_hev_cycle)
async def async_set_waveform_optional(
self, value: dict[str, Any], rapid: bool = False
) -> None:
+8 -16
View File
@@ -28,7 +28,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_point_in_utc_time
import homeassistant.util.color as color_util
from .const import DATA_LIFX_MANAGER, DOMAIN
from .const import ATTR_INFRARED, ATTR_POWER, ATTR_ZONES, DATA_LIFX_MANAGER, DOMAIN
from .coordinator import LIFXUpdateCoordinator
from .entity import LIFXEntity
from .manager import (
@@ -39,13 +39,7 @@ from .manager import (
)
from .util import convert_8_to_16, convert_16_to_8, find_hsbk, lifx_features, merge_hsbk
SERVICE_LIFX_SET_STATE = "set_state"
COLOR_ZONE_POPULATE_DELAY = 0.3
ATTR_INFRARED = "infrared"
ATTR_ZONES = "zones"
ATTR_POWER = "power"
LIFX_STATE_SETTLE_DELAY = 0.3
SERVICE_LIFX_SET_STATE = "set_state"
@@ -225,18 +219,16 @@ 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)
# Avoid state ping-pong by holding off updates as the state settles
await asyncio.sleep(LIFX_STATE_SETTLE_DELAY)
# Update when the transition starts and ends
await self.update_during_transition(fade)
@@ -344,7 +336,7 @@ class LIFXStrip(LIFXColor):
# Zone brightness is not reported when powered off
if not self.is_on and hsbk[HSBK_BRIGHTNESS] is None:
await self.set_power(True)
await asyncio.sleep(COLOR_ZONE_POPULATE_DELAY)
await asyncio.sleep(LIFX_STATE_SETTLE_DELAY)
await self.update_color_zones()
await self.set_power(False)
@@ -1,7 +1,7 @@
"""The Litter-Robot integration."""
from __future__ import annotations
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, LitterRobot4
from pylitterbot import FeederRobot, LitterRobot, LitterRobot3, Robot
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
@@ -10,65 +10,48 @@ from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .hub import LitterRobotHub
PLATFORMS = [
Platform.BUTTON,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
]
PLATFORMS_BY_TYPE = {
LitterRobot: (
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
),
LitterRobot3: (
Platform.BUTTON,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
),
LitterRobot4: (
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.VACUUM,
),
FeederRobot: (
Platform.BUTTON,
Robot: (
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
),
LitterRobot: (Platform.VACUUM,),
LitterRobot3: (Platform.BUTTON,),
FeederRobot: (Platform.BUTTON,),
}
def get_platforms_for_robots(robots: list[Robot]) -> set[Platform]:
"""Get platforms for robots."""
return {
platform
for robot in robots
for robot_type, platforms in PLATFORMS_BY_TYPE.items()
if isinstance(robot, robot_type)
for platform in platforms
}
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Litter-Robot from a config entry."""
hass.data.setdefault(DOMAIN, {})
hub = hass.data[DOMAIN][entry.entry_id] = LitterRobotHub(hass, entry.data)
await hub.login(load_robots=True)
platforms: set[str] = set()
for robot in hub.account.robots:
platforms.update(PLATFORMS_BY_TYPE[type(robot)])
if platforms:
if platforms := get_platforms_for_robots(hub.account.robots):
await hass.config_entries.async_forward_entry_setups(entry, platforms)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id]
await hub.account.disconnect()
platforms = get_platforms_for_robots(hub.account.robots)
unload_ok = await hass.config_entries.async_unload_platforms(entry, platforms)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
+24 -28
View File
@@ -1,21 +1,25 @@
"""Support for Litter-Robot button."""
from __future__ import annotations
from collections.abc import Callable, Coroutine, Iterable
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
import itertools
from typing import Any, Generic
from pylitterbot import FeederRobot, LitterRobot3
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.components.button import (
DOMAIN as PLATFORM,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .entity import LitterRobotEntity, _RobotT
from .entity import LitterRobotEntity, _RobotT, async_update_unique_id
from .hub import LitterRobotHub
@@ -26,21 +30,24 @@ async def async_setup_entry(
) -> None:
"""Set up Litter-Robot cleaner using config entry."""
hub: LitterRobotHub = hass.data[DOMAIN][entry.entry_id]
entities: Iterable[LitterRobotButtonEntity] = itertools.chain(
(
LitterRobotButtonEntity(
robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON
)
for robot in hub.litter_robots()
if isinstance(robot, LitterRobot3)
),
(
LitterRobotButtonEntity(
robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON
)
for robot in hub.feeder_robots()
),
entities: list[LitterRobotButtonEntity] = list(
itertools.chain(
(
LitterRobotButtonEntity(
robot=robot, hub=hub, description=LITTER_ROBOT_BUTTON
)
for robot in hub.litter_robots()
if isinstance(robot, LitterRobot3)
),
(
LitterRobotButtonEntity(
robot=robot, hub=hub, description=FEEDER_ROBOT_BUTTON
)
for robot in hub.feeder_robots()
),
)
)
async_update_unique_id(hass, PLATFORM, entities)
async_add_entities(entities)
@@ -76,17 +83,6 @@ class LitterRobotButtonEntity(LitterRobotEntity[_RobotT], ButtonEntity):
entity_description: RobotButtonEntityDescription[_RobotT]
def __init__(
self,
robot: _RobotT,
hub: LitterRobotHub,
description: RobotButtonEntityDescription[_RobotT],
) -> None:
"""Initialize a Litter-Robot button entity."""
assert description.name
super().__init__(robot, description.name, hub)
self.entity_description = description
async def async_press(self) -> None:
"""Press the button."""
await self.entity_description.press_fn(self.robot)
+35 -15
View File
@@ -1,7 +1,7 @@
"""Litter-Robot entities for common data and methods."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from collections.abc import Callable, Coroutine, Iterable
from datetime import time
import logging
from typing import Any, Generic, TypeVar
@@ -10,8 +10,9 @@ from pylitterbot import Robot
from pylitterbot.exceptions import InvalidCommandException
from typing_extensions import ParamSpec
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.entity import DeviceInfo, EntityCategory, EntityDescription
import homeassistant.helpers.entity_registry as er
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
@@ -36,18 +37,18 @@ class LitterRobotEntity(
_attr_has_entity_name = True
def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None:
def __init__(
self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription
) -> None:
"""Pass coordinator to CoordinatorEntity."""
super().__init__(hub.coordinator)
self.robot = robot
self.entity_type = entity_type
self.hub = hub
self._attr_name = entity_type.capitalize()
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return f"{self.robot.serial}-{self.entity_type}"
self.entity_description = description
self._attr_unique_id = f"{self.robot.serial}-{description.key}"
# The following can be removed in 2022.12 after adjusting names in entities appropriately
if description.name is not None:
self._attr_name = description.name.capitalize()
@property
def device_info(self) -> DeviceInfo:
@@ -65,9 +66,11 @@ class LitterRobotEntity(
class LitterRobotControlEntity(LitterRobotEntity[_RobotT]):
"""A Litter-Robot entity that can control the unit."""
def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None:
def __init__(
self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription
) -> None:
"""Init a Litter-Robot control entity."""
super().__init__(robot=robot, entity_type=entity_type, hub=hub)
super().__init__(robot=robot, hub=hub, description=description)
self._refresh_callback: CALLBACK_TYPE | None = None
async def perform_action_and_refresh(
@@ -134,9 +137,11 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]):
_attr_entity_category = EntityCategory.CONFIG
def __init__(self, robot: _RobotT, entity_type: str, hub: LitterRobotHub) -> None:
def __init__(
self, robot: _RobotT, hub: LitterRobotHub, description: EntityDescription
) -> None:
"""Init a Litter-Robot control entity."""
super().__init__(robot=robot, entity_type=entity_type, hub=hub)
super().__init__(robot=robot, hub=hub, description=description)
self._assumed_state: bool | None = None
async def perform_action_and_assume_state(
@@ -146,3 +151,18 @@ class LitterRobotConfigEntity(LitterRobotControlEntity[_RobotT]):
if await self.perform_action_and_refresh(action, assumed_state):
self._assumed_state = assumed_state
self.async_write_ha_state()
def async_update_unique_id(
hass: HomeAssistant, domain: str, entities: Iterable[LitterRobotEntity[_RobotT]]
) -> None:
"""Update unique ID to be based on entity description key instead of name.
Introduced with release 2022.9.
"""
ent_reg = er.async_get(hass)
for entity in entities:
old_unique_id = f"{entity.robot.serial}-{entity.entity_description.name}"
if entity_id := ent_reg.async_get_entity_id(domain, DOMAIN, old_unique_id):
new_unique_id = f"{entity.robot.serial}-{entity.entity_description.key}"
ent_reg.async_update_entity(entity_id, new_unique_id=new_unique_id)

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