Compare commits

...

236 Commits

Author SHA1 Message Date
Robert Resch 573644459a Merge branch 'dev' into edenhaus-ecovacs-quality-scale 2025-05-19 15:03:11 +02:00
Simone Chemelli e64f76bebe Add full test coverage for Comelit cover (#144761) 2025-05-19 15:01:41 +02:00
epenet 7c5090d627 Add missing type hint in zestimate (#145218) 2025-05-19 14:55:48 +02:00
Robert Resch f6a0d630c3 Fix typo in Ecovacs get_supported_entities (#145215) 2025-05-19 14:45:30 +02:00
markhannon 880f5faeec Add cover entity to Zimi integration (#144330) 2025-05-19 14:24:25 +02:00
Retha Runolfsson 0cf503d871 Add exception translation for switchbot device initialization (#144828) 2025-05-19 14:22:10 +02:00
epenet 9d050360c8 Prevent import from syrupy.SnapshotAssertion (#145208) 2025-05-19 14:18:35 +02:00
Simone Chemelli 0c0c61f9e0 Bump aiocomelit to 0.12.3 (#145209) 2025-05-19 14:16:12 +02:00
Erik Montnemery e868b3e8ff Sort and simplify DeletedRegistryEntry (#145207) 2025-05-19 14:13:57 +02:00
Simone Chemelli 555215a848 Update quality_scale rules status for Comelit (#143592) 2025-05-19 14:05:08 +02:00
Simone Chemelli 484a547758 Fix pylance warning on SnapshotAssertion import (#145206) 2025-05-19 13:55:48 +02:00
wuede 7d25f68fa5 update pyatmo to version 9.2.0 (#145203) 2025-05-19 13:21:19 +02:00
Maikel Punie 8b22ab93c1 Bump velbusaio to 2025.5.0 (#145198) 2025-05-19 13:20:02 +02:00
epenet 78e3a2d0c6 Mark all _CLASS_MATCH as mandatory in pylint plugin (#145200) 2025-05-19 12:12:17 +01:00
Robert Resch 241c89e885 Bump go2rtc-client to 0.1.3b0 (#145192) 2025-05-19 12:11:07 +01:00
Marc Mueller 7d96a2a620 [ci] Skip step if coverage is skipped (#145202) 2025-05-19 12:46:38 +02:00
Martin Hjelmare 08104eec56 Fix Z-Wave unique id update during controller migration (#145185) 2025-05-19 13:43:06 +03:00
Michael 0fc81d6b33 Add diagnostics platform to Immich integration (#145162)
* add diagnostics platform

* also redact host
2025-05-19 12:23:04 +02:00
Joost Lekkerkerker cb84e55c34 Map auto to heat_cool for thermostat in SmartThings (#145098) 2025-05-19 12:09:27 +02:00
Joost Lekkerkerker 68c3d5a159 Add lamp capability for hood component in SmartThings (#145036) 2025-05-19 12:07:50 +02:00
epenet 77bab39ed0 Move downloader service to separate module (#145183) 2025-05-19 12:05:33 +02:00
Maciej Bieniek 92e570ffc1 Revert "Link Shelly device entry with Shelly BT scanner entry (#144626)" (#145177)
This reverts commit b15c9ad130.
2025-05-19 13:01:54 +03:00
Paulus Schoutsen 919684e20a Minor cleanup for pipeline tts stream test (#145146) 2025-05-19 11:58:58 +02:00
G Johansson a1d6df6ce9 Remove deprecated aux heat from ephember (#145152) 2025-05-19 11:58:35 +02:00
epenet 07c3c3bba8 Mark type hint as compulsory for entity.assumed_state property (#145187) 2025-05-19 11:56:05 +02:00
epenet f11e040662 Mark all _FUNCTION_MATCH as mandatory in pylint plugin (#145194) 2025-05-19 11:55:15 +02:00
epenet 8d83341308 Mark type hint as compulsory for entity.available property (#145189) 2025-05-19 11:50:41 +02:00
Erik Montnemery f27b2c4df1 Improve device registry restore tests (#145186) 2025-05-19 11:06:16 +02:00
Matrix 717b84bab9 Add battery entity for LockV2 in yolink (#145169)
Add battery entity for LockV2
2025-05-19 11:01:30 +02:00
epenet a34bce6202 Fix runtime_data in iqvia (#145181) 2025-05-19 10:59:46 +02:00
epenet bd190b9b4c Use runtime_data in icloud (#145179) 2025-05-19 10:59:06 +02:00
epenet da6c6c5201 Use runtime_data in ialarm (#145178) 2025-05-19 10:58:34 +02:00
epenet f50afae1c3 Use runtime_data in hvv_departures (#144951) 2025-05-19 10:58:01 +02:00
epenet 177afea5ad Use runtime_data in huisbaasje (#144953) 2025-05-19 10:57:22 +02:00
Joost Lekkerkerker a3aae68229 Add athmospheric pressure capability to SmartThings (#145103) 2025-05-19 10:41:22 +02:00
Robert Resch 9ff9d9230e Fix test results parsing error (#145077) 2025-05-19 10:40:03 +02:00
epenet 2bb0843c30 Add ability to mark type hints as compulsory on specific functions (#139730) 2025-05-19 10:27:07 +02:00
Joakim Sørensen 5f2425f421 Bump hass-nabucasa from 0.100.0 to 0.101.0 (#145172) 2025-05-19 10:24:08 +02:00
J. Nick Koston e46ca41697 Bump aioesphomeapi to 31.1.0 (#145170) 2025-05-19 10:22:47 +02:00
dependabot[bot] fa5a7aea7e Bump github/codeql-action from 3.28.17 to 3.28.18 (#145173)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 10:14:37 +02:00
Tsvi Mostovicz 030681a443 Jewish calendar: use const in action code (#145007)
* Use const defines in code

* Added exception raises

* Revert "Added exception raises"

This reverts commit e8849e586c83b45ecfd374986edb0d8c64b263e4.
2025-05-19 10:14:22 +02:00
epenet aa3cbf2473 Cleanup unused string in samsungtv (#145174) 2025-05-19 09:10:01 +02:00
Erik Montnemery ce71f6444c Sort and simplify DeletedDeviceEntry (#145171)
* Sort and simplify DeletedDeviceEntry

* Fix sort

* Fix sort
2025-05-19 08:40:22 +02:00
J. Nick Koston eb4d561b96 Bump grpcio to 1.72.0 and protobuf to 6.30.2 (#143633) 2025-05-18 20:10:38 -04:00
peteS-UK 075a41c69a Fix album and artist returning "None" rather than None for Squeezebox media player. (#144971)
* fix

* snapshot update

* cast type
2025-05-18 23:37:06 +02:00
G Johansson 2ba2248f67 Remove deprecated aux heat from econet (#145149) 2025-05-18 23:03:13 +02:00
starkillerOG ff5ed82de8 Add Kaiser Nienhaus virtual motionblinds integration (#145096)
* Add Kaiser Nienhaus virtual motionblinds integration

* fix typo
2025-05-18 23:01:02 +02:00
wuede 541b969d3b Netatmo: do not fail on schedule updates (#142933)
* do not fail on schedule updates

* add test to check that the store data remains unchanged
2025-05-18 23:00:36 +02:00
javicalle 3d83c6299b Enable RFDEBUG on RFLink "Enable debug logging" (#138571)
* Enable RFDEBUG on "Enable debug logging"

* fix checks

* fix checks

* one more lap

* fix test

* wait to init rflink 

In my dev env this is not needed

* use hass.async_create_task(handle_logging_changed())

instead async_at_started(hass, handle_logging_changed)

* revert unneeded async_block_till_done

* Remove the startup management

There's a race condition at startup that can't be managed nicely
2025-05-18 22:51:42 +02:00
generically-named 3ecde49dca Add energy/water forecast for Miele integration (#144822)
* Add energy/water forecast & fix drying_step error

Adding the energy forecast and water forecast entities that are present in the HACS version of this integration but absent in the HA Core implantation. 

Also fixed the state_drying_step sensor which wasn't handling casting of the API value to an int correctly due to the API sometimes giving a None value.

* Fix formatting issues from previous commit

* Fix missing translation_key line 202

* Remove icon entries

* Update icons.json

* Update strings.json

* Update strings.json (correcting mixed up energy/water forecast names)

* Update homeassistant/components/miele/strings.json

Co-authored-by: Åke Strandberg <ake@strandberg.eu>

* Update homeassistant/components/miele/strings.json

Co-authored-by: Åke Strandberg <ake@strandberg.eu>

* Fix tests

---------

Co-authored-by: Åke Strandberg <ake@strandberg.eu>
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-05-18 22:33:27 +02:00
Nils Müller c1fcd8ea7f Fix Nanoleaf light state propagation after change from home asisstant (#144291)
* Fix Nanoleaf light state propagation after change from home asisstant

* Add tests to check if nanoleaf light is triggering async_write_ha_state

* Fix pylint for test case

* Fix use coordinator.async_refresh instead of async_write_ha_state

* Fix tests

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-05-18 22:26:02 +02:00
G Johansson 78ac8ba841 Remove deprecated aux heat from Nexia (#145147) 2025-05-18 16:14:22 -04:00
Keilin Bickar d9cfab4c8e Bump sense-energy to 0.13.8 (#145156) 2025-05-18 21:45:11 +02:00
Oliver 4c10502b0e Update denonavr to 1.1.1 (#145155) 2025-05-18 21:44:53 +02:00
Michael a576f7baf3 Add Immich integration (#145125)
* add immich integration

* bump aioimmich==0.3.1

* rework to require an url as input and pare it afterwards

* fix doc strings

* remove name attribute from deviceinfo as it is default behaviour

* add translated uom for count sensors

* explicitly pass in the config_entry in coordinator

* fix url in strings to uppercase

* use data_updates attribute instead of data

* remove left over

* match entries only by host

* remove quotes

* import SOURCE_USER directly, instead of config_entries

* split happy and sad flow tests

* remove unneccessary async_block_till_done() calls

* replace url example by "full URL"

* bump aioimmich==0.4.0

* bump aioimmich==0.5.0

* allow multiple users for same immich instance

* Fix tests

* limit entities when user has no admin rights

* Fix tests

* Fix tests

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-05-18 21:28:15 +02:00
G Johansson 520c964656 Remove deprecated aux heat from elkm1 (#145148) 2025-05-18 20:50:33 +02:00
Matrix 3f59b1c376 Add YoLink new device types support 5009 & 5029 (#144323)
* Leak Stop

* Fix as suggested.
2025-05-18 19:59:19 +02:00
Markus Adrario 3ff095cc51 Add Homee alarm-control-panel platform (#140041)
* Add alarm control panel

* Add alarm control panel tests

* add disarm function

* reuse state setting code

* change sleeping to night

* review change 1

* fix review comments

* fix review comments
2025-05-18 17:25:09 +02:00
Marc Hörsken aa4c41abe8 Postpone update in WMSPro after service call (#144836)
* Reduce stress on WMS WebControl pro with higher scan interval

Avoid delays and connection issues due to overloaded hub.
Fixes #133832 and #134413

* Schedule an entity state update after performing an action

Avoid delaying immediate status updates, e.g. on/off changes.

* Replace scheduled state updates with delayed action completion

Suggested-by: joostlek
2025-05-18 17:23:21 +02:00
Sid 906b3901fb Add select platform to eheimdigital (#145031)
* Add select platform to eheimdigital

* Review

* Review

* Fix tests
2025-05-18 16:52:27 +02:00
Andre Lengwenus 2aba4f261f Add has_entity_name attribute to LCN entities (#145045)
* Add _attr_has_entity_name

* Fix tests
2025-05-18 16:48:44 +02:00
elmurato 3eb0c8ddff Add Pterodactyl binary sensor tests (#142401)
* Add binary sensor tests

* Wait for background tasks as well in test_binary_sensor_update_failure

* Fix module docstring

* Use snapshot_platform, move constants to const.py, do not use snapshot for testing state updates

* Use JSON fixtures

* Use helper for loading JSON fixtures, remove unneeded mock in setup_integration

* Move mocks to pytest markers where possible
2025-05-18 16:46:11 +02:00
Andrea Turri 705a987547 Fix enum values for program phases by appliance type on Miele appliances (#144916) 2025-05-18 11:00:21 +02:00
J. Nick Koston 888f17c504 Bump google-maps-routing to 0.6.15 (#145130) 2025-05-18 09:11:13 +02:00
markhannon 2f4d0ede0f Bump zcc-helper to 3.5.2 (#144926) 2025-05-18 07:13:23 +02:00
Paulus Schoutsen 6fd9857666 OpenAI Conversation split out chat log processing (#145129) 2025-05-17 22:00:24 -07:00
XiaoXianNv-boot f07265ece4 Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration (#144295)
* Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration

* Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration

* Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration

* Set the default upgrade icon for the MQTT device to the default icon for Home Assistant instead of the icon for the MQTT integration

* Fix failed tests

* Fix failed tests

* Cleanup unused helper option

* ruff

---------

Co-authored-by: jbouwh <jan@jbsoft.nl>
2025-05-18 01:30:04 +02:00
J. Nick Koston a169d6ca97 Bump cryptography to 45.0.1 and pyopenssl to 25.1.0 (#145121) 2025-05-17 23:57:28 +02:00
Joost Lekkerkerker ebed38c1dc Add Steam closet sanitize to SmartThings (#145110) 2025-05-17 21:03:24 +02:00
Joost Lekkerkerker 5619042fe7 Add Steam closet auto cycle link to SmartThings (#145111) 2025-05-17 20:39:17 +02:00
Joost Lekkerkerker 67b3428b07 Add Steam closet keep fresh mode to SmartThings (#145107) 2025-05-17 20:19:31 +02:00
Jan-Philipp Benecke 2302a3bb33 Add missing device condition translations to lock component (#145104) 2025-05-17 20:18:14 +02:00
Åke Strandberg a83eafd949 Fix mapping from program_phase to vacuum_activity for Miele integration (#145115) 2025-05-17 20:17:15 +02:00
Paulus Schoutsen 2956f4fea1 Ensure that OpenAI tool call deltas have a role (#145085) 2025-05-17 09:36:14 -07:00
Robert Resch 180e1f462c Fix proberly Ecovacs mower area sensors (#145078) 2025-05-17 16:44:53 +02:00
cdnninja 2dc63eb8c5 Refactor fan in vesync (#135744)
* Refactor Fan

* Add tower fan tests and mode

* Schedule update after turn off

* Adjust updates to refresh library

* correct off command

* Revert changes

* Merge corrections

* Remove unused code to increase test coverage

* Ruff

* Tests

* Test for preset mode

* Adjust to increase coverage

* Test Corrections

* tests to match other PR

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-17 15:57:55 +02:00
Manu 4c40ec4948 Bump aiontfy to 0.5.2 (#145044) 2025-05-17 13:06:02 +02:00
starkillerOG 56b3dc02a7 Bump motionblinds to 0.6.27 (#145094) 2025-05-17 12:45:18 +02:00
Franck Nijhof db5bcd9fc4 Pin rpds-py to 0.24.0 (#145074) 2025-05-16 22:39:05 +02:00
jb101010-2 c845f4e9b2 Bump pysuezV2 to 2.0.5 (#145047) 2025-05-16 22:33:14 +02:00
Joost Lekkerkerker 5aff3499a0 Add number entities for freezer setpoint in SmartThings (#145069) 2025-05-16 22:29:00 +02:00
Andre Lengwenus a501451038 Remove address parameter from services.yaml (#145052) 2025-05-16 22:27:09 +02:00
Paulus Schoutsen 0deed82bea OpenAI prompt is optional (#145065) 2025-05-16 22:22:46 +02:00
starkillerOG f9231de824 Add additional explanation for Reolink password requirements (#145000) 2025-05-16 22:12:59 +02:00
Joost Lekkerkerker 757c66613d Deprecate SmartThings water heater sensors (#145060) 2025-05-16 21:59:12 +02:00
epenet 9d2302f2f5 Use runtime_data in homeworks (#144944) 2025-05-16 21:57:36 +02:00
epenet 0bbbd2cd54 Use runtime_data in hydrawise (#144950) 2025-05-16 21:45:11 +02:00
Robert Resch dbc15a2dda Fix Ecovacs mower area sensors (#145071) 2025-05-16 21:22:43 +02:00
Joost Lekkerkerker 7fefd58b84 Don't create entities for Smartthings smarttags (#145066) 2025-05-16 21:17:07 +02:00
Joost Lekkerkerker 87b60967a6 Map SmartThings auto mode correctly (#145061) 2025-05-16 20:14:41 +02:00
Joost Lekkerkerker e80069545f Only set suggested area for new SmartThings devices (#145063) 2025-05-16 19:53:46 +02:00
Joost Lekkerkerker be5685695e Fix fan AC mode in SmartThings AC (#145064) 2025-05-16 19:38:18 +02:00
Bram Kragten 6b769ac263 Update frontend to 20250516.0 (#145062) 2025-05-16 19:37:22 +02:00
Simone Chemelli 9114816384 Fix climate idle state for Comelit (#145059) 2025-05-16 18:51:30 +02:00
Ludovic BOUÉ db3e596e48 Update Matter MicrowaveOven fixture (#145057)
Update microwave_oven.json

PowerInWatts feature
2025-05-16 18:19:36 +02:00
Joost Lekkerkerker bdc21da076 Sync SmartThings EHS fixture (#145042) 2025-05-16 15:08:24 +02:00
epenet a500eeb831 Use runtime_data in hue (#144946)
* Use runtime_data in hue

* More

* Tests
2025-05-16 08:35:46 -04:00
Joost Lekkerkerker 119d0c576a Add hood fan speed capability to SmartThings (#144919) 2025-05-16 13:39:03 +02:00
Bouwe Westerdijk 38cee53999 Small code optimization for Plugwise (#145037) 2025-05-16 13:28:31 +02:00
Joost Lekkerkerker 2ca9d4689e Set SmartThings oven setpoint to unknown if its 1 Fahrenheit (#145038) 2025-05-16 13:17:56 +02:00
Joost Lekkerkerker 8a32ffc7b9 Bump pySmartThings to 3.2.2 (#145033) 2025-05-16 13:10:58 +02:00
Matthias Alphart 6475b1a446 Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue for new setups (#144940) 2025-05-16 12:58:59 +02:00
starkillerOG 07db244f91 Cleanup wrongly combined Reolink devices (#144771) 2025-05-16 12:58:28 +02:00
Joost Lekkerkerker ff4aed1f6e Fix errors in strings in SmartThings (#145030) 2025-05-16 12:22:17 +02:00
Joost Lekkerkerker 3208815e10 Fix non-DHW heat pump in SmartThings (#145008) 2025-05-16 12:08:32 +02:00
epenet b4a1bdcb83 Move huisbaasje coordinator to separate module (#144955) 2025-05-16 12:07:19 +02:00
epenet 97869636f8 Use typed config entry in Habitica coordinator (#144956) 2025-05-16 11:59:11 +02:00
epenet cbb092f792 Move icloud services to separate module (#144980) 2025-05-16 11:56:07 +02:00
Sanjay Govind 0c5ee37721 Update bosch_alarm door switch strings so they are more user friendly (#144607)
* Update door switch strings so they are more user friendly

* Update door switch strings so they are more user friendly

* Update door switch strings so they are more user friendly

* update strings

* update strings
2025-05-16 11:43:31 +02:00
epenet e74aeeab1a Use runtime_data in iaqualink (#144988) 2025-05-16 11:41:16 +02:00
Sanjay Govind b8df9c7e97 Set parallel_updates for bosch_alarm (#145028) 2025-05-16 11:26:22 +02:00
epenet 82a9e67b7e Use generic in iaqualink entity (#144989) 2025-05-16 10:53:24 +02:00
Joost Lekkerkerker 7410b8778a Deprecate DHW switch for SmartThings (#145011) 2025-05-16 10:47:23 +02:00
epenet 3e92f23680 Cleanup huisbaasje tests (#144954) 2025-05-16 10:38:17 +02:00
rjblake 3942e6a841 Fix some Home Connect translation strings (#144905)
* Update strings.json

Corrected program names:
changed "Pre_rinse" to "Pre-Rinse"
changed "Kurz 60°C" to "Speed 60°C"

Both match the Home Connect app; although the UK documentation refers to "Speed 60°C" as "Quick 60°C"

* Adjust casing

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-05-16 10:37:11 +02:00
Joost Lekkerkerker e76b483067 Add lamp capability to SmartThings (#144918) 2025-05-16 10:36:58 +02:00
Retha Runolfsson 3de740ed1e Bump PySwitchbot to 0.62.2 (#145018) 2025-05-16 10:30:30 +02:00
Bouwe Westerdijk bbe975baef Bump plugwise to v1.7.4 (#145021) 2025-05-16 10:28:57 +02:00
Sid 6dff975711 Initialize select _attr_current_option with None (#145026) 2025-05-16 10:27:59 +02:00
Jan Bouwhuis 71108d9ca0 Do not show an empty component name on MQTT device subentries not as None if it is not set (#144792) 2025-05-16 10:26:00 +02:00
puddly 053e5417a7 Strip _CLIENT suffix from ZHA event unique_id (#145006) 2025-05-16 10:25:24 +02:00
Sanjay Govind 9bbc49e842 Add DHCP discovery flow to bosch_alarm (#142250)
* Add dhcp discovery

* Update homeassistant/components/bosch_alarm/config_flow.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* put mac address in entry instead of unique id

* Update host and mac via dhcp discovery

* add mac to connections

* Abort dhcp flow if there is already an ongoing flow

* apply changes from review

* apply change from review

* remove outdated test

* fix snapshots

* apply change from review

* clean definition for connections

* update quality scale

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-16 10:21:41 +02:00
dependabot[bot] 270780ef5f Bump docker/build-push-action from 6.16.0 to 6.17.0 (#145022)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 09:42:24 +02:00
dependabot[bot] e15963b422 Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#145023)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 08:54:13 +02:00
starkillerOG 52e8196d0a Mark Reolink doorbell visitor sensor as always available (#145002)
Mark doorbell visitor sensor as always available
2025-05-15 20:34:55 -04:00
Odd Stråbø cc62943835 Fix ESPHome entities unavailable if deep sleep enabled after entry setup (#144970) 2025-05-15 18:57:16 -05:00
epenet d195726ed2 Use runtime_data in isy994 (#144961) 2025-05-15 13:50:48 -05:00
epenet 50e6c83dd8 Fix missing mock in hue v2 bridge tests (#144947) 2025-05-15 13:53:12 -04:00
alorente 3a58d97496 Fix wrong UNIT_CLASS for reactive energy converter (#144982) 2025-05-15 18:27:16 +01:00
epenet ace12958d1 Use runtime_data in iqvia (#144984) 2025-05-15 17:48:02 +02:00
Joost Lekkerkerker d33a0f75fd Add water heater support to SmartThings (#144927)
* Add another EHS SmartThings fixture

* Add another EHS

* Add water heater support to SmartThings

* Add water heater support to SmartThings

* Add water heater support to SmartThings

* Add water heater support to SmartThings

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Fix

* Add more tests

* Make target temp setting conditional

* Make target temp setting conditional

* Finish tests

* Fix
2025-05-15 17:42:38 +02:00
Erik Montnemery d24a60777b Fix Home Assistant Yellow config entry data (#144948) 2025-05-15 10:07:53 -04:00
epenet f2a3a5cbbd Move iqvia coordinator to separate module (#144969)
* Move iqvia coordinator to separate module

* Adjust
2025-05-15 15:50:46 +02:00
Petro31 3bf9908789 Add template vacuum modern style (#144843)
* Add template vacuum modern style

* address comments and add tests for coverage

* address comments

* update vacuum and sort domains
2025-05-15 15:46:00 +02:00
epenet 912798ee34 Use runtime_data in intellifire (#144979) 2025-05-15 14:57:26 +02:00
epenet 28990e1db5 Use runtime_data in ipma (#144972)
* Use runtime_data in ipma

* Cleanup const
2025-05-15 14:43:58 +02:00
epenet e8281bb009 Use runtime_data in iotawatt (#144977) 2025-05-15 14:43:35 +02:00
Robert Resch 334f9deaec Bump deebot-client to 13.2.0 (#144957) 2025-05-15 13:46:15 +02:00
alorente 1d47dc41c9 Add reactive energy device class and units (#143941) 2025-05-15 12:05:46 +01:00
Petro31 66ecc4d69d Add modern configuration for template alarm control panel (#144834)
* Add modern configuration for template alarm control panel

* address comments and add tests for coverage

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-05-15 11:46:57 +02:00
starkillerOG fa3edb5c01 Fix Netgear handeling of missing MAC in device registry (#144722) 2025-05-15 10:56:54 +02:00
Petro31 ea046f32be Add modern style template lock (#144756)
* Add modern style lock

* add tests

* Add tests and address comments

* Update homeassistant/components/template/lock.py

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-05-15 10:43:56 +02:00
markhannon fd09476b28 Add sensor entity to Zimi integration (#144329)
* Import sensor.py

* Light design alignment

* Fix merge error

* Refactor with extend

* Update homeassistant/components/zimi/sensor.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

* value_fn and inline refactoring

* strings.json and translation_key

* Add sensor_name

* Revert "Add sensor_name"

This reverts commit ad3da048e9c5a6ecdb15052c253de7dc46b1120f.

* Default naming for sensors

* Remove uneeded 'garage' and use default battery name

* Bump to zcc-helper 3.5.2 which maps "Garage Controller" tp "Garage" in device.name

* Update homeassistant/components/zimi/sensor.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Update homeassistant/components/zimi/sensor.py

Co-authored-by: Josef Zweck <josef@zweck.dev>

* Update strings.json

* Revert "Bump to zcc-helper 3.5.2 which maps "Garage Controller" tp "Garage" in device.name"

This reverts commit 345ef8a4859c8d0e188462c9a69a4fab8f284b69.

* Update homeassistant/components/zimi/sensor.py

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-05-15 10:12:18 +02:00
Alexandre CUER 7c306acd5d Emoncms remove useless var in tests (#144942) 2025-05-15 09:48:01 +02:00
G Johansson 9c4733595a Fix unknown Pure AQI in Sensibo (#144924)
* Fix unknown Pure AQI in Sensibo

* Fix mypy
2025-05-15 10:27:48 +03:00
Petro31 c7cf9585ae Add modern style configuration for template fan (#144751)
* add modern template fan

* address comments and add tests for coverage
2025-05-15 08:18:37 +02:00
J. Nick Koston 301ca88f41 Bump aioesphomeapi to 31.0.1 (#144939) 2025-05-14 22:27:25 -05:00
peteS-UK 9a0fed89bd Translate raised exceptions for Squeezebox (#144842)
* initial

* tweak

* review updates
2025-05-14 19:39:00 -04:00
Joost Lekkerkerker 2050b0b375 Add another EHS SmartThings fixture (#144920)
* Add another EHS SmartThings fixture

* Add another EHS
2025-05-14 23:23:18 +02:00
epenet 34c7c3f384 Use runtime_data in homematicip_cloud (#144892) 2025-05-14 23:14:02 +02:00
epenet 3b9d8e00bc Use runtime_data and HassKey in geofency (#144886) 2025-05-14 23:13:37 +02:00
Abílio Costa 6b35b069b2 Remove duplicated code in unit conversion util (#144912) 2025-05-14 22:05:29 +01:00
Paulus Schoutsen 9428127021 Add media search and play intent (#144269)
* Add media search intent

* Add PLAY_MEDIA as required feature and remove explicit responses

---------

Co-authored-by: Michael Hansen <mike@rhasspy.org>
2025-05-14 15:45:40 -04:00
Sanjay Govind 1e8843947c Add sensor for alarm status in bosch_alarm (#142564)
* Add sensor for alarm status

* style fixes

* fix icons

* style fixes

* update tests

* apply change from code review

* add alarm to alarm sensor state

* Apply changes from review
2025-05-14 21:00:41 +02:00
Sanjay Govind dbdffbba23 Add binary sensors to bosch_alarm (#142147)
* Add binary sensors to bosch_alarm

* make one device per sensor, remove device class guessing

* fix tests

* update tests

* Apply suggested changes

* add binary sensors

* make fault sensors diagnostic

* update tests

* update binary sensors to use base entity

* fix strings

* fix icons

* add state translations for area ready sensors

* use constants in tests

* apply changes from review

* remove fault prefix, use default translation for battery low

* update tests
2025-05-14 20:56:08 +02:00
Daniel Hjelseth Høyer 460f02ede5 Update mill library 0.12.5 (#144911)
* Update mill library 0.12.5

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Update mill library 0.12.5

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

---------

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-05-14 20:46:28 +02:00
Sid 0eb6c88bc5 Add system LED brightness to eheimdigital (#144915) 2025-05-14 20:45:58 +02:00
Nick Kuiper 4b7650f2d2 Add buttons to Blue current integration (#143964)
* Add buttons to Blue current integration

* Apply feedback

* Changed configEntry to use the BlueCurrentConfigEntry.

The connector is now accessed via the entry instead of hass.data.

* Changed test_buttons_created test to use the snapshot_platform function.

Also removed the entry.unique_id check in the test_charge_point_buttons function because this is not needed anymore, according to https://github.com/home-assistant/core/pull/114000#discussion_r1627201872

* Applied requested changes.

Changes requested by joostlek.

* Moved has_value from BlueCurrentEntity to class level.

This value was still inside the __init__ function, so the value was not overwritten by the ChargePointButton.

---------

Co-authored-by: Floris272 <florispuijk@outlook.com>
2025-05-14 19:37:16 +02:00
Daniel Hjelseth Høyer 8004c6605b Update Tibber lib 0.31.2 (#144908)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2025-05-14 19:25:01 +02:00
Marc Hörsken 9d451b6358 Add support for identify buttons to WMS WebControl pro (#143339)
* Remove _attr_name = None from generic base class

* Add support for identify buttons to WMS WebControl pro

* Fix PERF401 as suggested by joostlek

* Fix fixture name after rebase

* Split test

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-05-14 18:06:21 +02:00
LG-ThinQ-Integration 7963665c40 Add fan for ventilator (#142444)
Add ventilator device

Co-authored-by: yunseon.park <yunseon.park@lge.com>
2025-05-14 17:58:25 +02:00
Erik Montnemery d44a34ce1e Refactor DeviceAutomationTriggerProtocol (#144888) 2025-05-14 17:24:19 +02:00
Joost Lekkerkerker 49b7559b1f Fix snapshots in APC (#144901) 2025-05-14 17:14:57 +02:00
Glenn Vandeuren (aka Iondependent) 43b1dd64a7 Handle unit conversion in lib for niko_home_control (#141837)
* handle unit conversion in lib

* bump lib

* Fix

---------

Co-authored-by: Joostlek <joostlek@outlook.com>
2025-05-14 17:13:06 +02:00
Matthias Alphart 2d0c1fac24 Fix "tunneling" spelling in KNX (#144895) 2025-05-14 17:05:45 +02:00
Andre Lengwenus a0f35a84ae Positioning for LCN covers (#143588)
* Fix motor control function names

* Add position logic for BS4

* Use helper methods from pypck

* Add motor positioning to domain_data schema

* Fix tests

* Add motor positioning via module

* Invert motor cover positions

* Merge relay cover classes back into one class

* Update snapshot for covers

* Revert bump lcn-frontend to 0.2.4
2025-05-14 16:49:30 +02:00
epenet 4bc5987f36 Use runtime_data in rachio (#144896) 2025-05-14 16:46:36 +02:00
Yuxin Wang 11644d48ee Use snapshot testing for APCUPSD integration (#130770)
* First try to use snapshot testing for sensors

* Use snapshot testing

* Add ambr files

* Update comment

* Address review comments

* Remove duplicate async init integration call

* Add device test for cases w/o SERIALNO

* Use friendlier snapshot names

* Use * to mandate keyed argument for async_init_integration

* Always pass mock config entry ID

* Fix incorrect ID
2025-05-14 16:04:07 +02:00
Petro31 d273a92a19 Refactor template optional configuration attributes (#144887) 2025-05-14 15:54:40 +02:00
Brian Rogers b0ff4b5841 Add flow detection to Rachio hose timer (#144075)
* flow binary sensor

* rename property

* Move const and update coordinator reference

* update controller descriptions

* Address review comments

* Use lookup for rain sensor

* Update online binary sensor

* make it a bit more readable

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-14 15:01:01 +02:00
epenet ef99658919 Use runtime_data in gpslogger (#144884) 2025-05-14 14:59:10 +02:00
epenet a9238c7577 Use entry.async_on_unload in gpslogger (#144883) 2025-05-14 14:31:50 +02:00
Brett Adams 993e98a43f Fix pin strings in Teslemetry (#144873)
pinstring
2025-05-14 14:31:41 +02:00
epenet 10dd11f257 Use HassKey in greeneye_monitor (#144878) 2025-05-14 14:30:45 +02:00
epenet fb9be3da79 Use entry.async_on_unload in geofency (#144882) 2025-05-14 14:30:02 +02:00
Åke Strandberg 3b1a33d606 Fix substitutions in strings.json in Miele integration (#144881)
Fix substitutions in strings.json
2025-05-14 14:14:48 +02:00
epenet 710e18f399 Use runtime_data in gree (#144880) 2025-05-14 14:06:40 +02:00
Maximilian Arzberger 67b9904740 Add Kostal plenticore Installer login support (#133773)
* feat: Add Installer login, Add ManualCharge Switch

* remove unnecessary field

* replace strings with consts

* change to CONF and camel_case

* Improve existing code

* Add translation string

* format code

* add service code test

* format code

* format code

* remove manual charge switch

* add reconfigure config flow

* fix flow

* add return type

* add reconfigure strings

* adjust tests

* change string

* simlify tests

* add reconfigure test

* add more tests

* Fix

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-14 14:05:23 +02:00
Robert Resch e413e9b93b Add mac address to airgradient devices (#144876) 2025-05-14 13:40:38 +02:00
Matthias Alphart 5c86042b31 Add Fronius current and voltage for up to 4 MPP trackers (#140120)
Support current and voltage of up to 4 MPP trackers
2025-05-14 13:37:02 +02:00
Martin Hjelmare e89333811e Improve Z-Wave config flow tests (#144871)
* Improve Z-Wave config flow tests

* Fix test

* Use identify check for result type
2025-05-14 13:08:26 +02:00
Dmytro Tkach 4f723232e3 Add modbus light brightness and color temperature (#139703)
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-14 12:07:19 +01:00
Åke Strandberg 48520d90ef Add plate sensors for Miele hobs (#144400)
* Add plate sensors for miele hobs

* Address review comments

* Update snapshot
2025-05-14 13:02:05 +02:00
Jeremiah Paige 2fdda91cb8 Fix pandora.media_player to not sleep during event loop (#141957)
* Fix pandora.media_player to not sleep during event loop

* factor out pianobar spawn

* linting cleanup

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-14 12:34:40 +02:00
Sören Beye c023f610dd Introduce recorder.get_statistics service (#142602)
Co-authored-by: abmantis <amfcalt@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Abílio Costa <abmantis@users.noreply.github.com>
2025-05-14 11:28:32 +01:00
epenet 161b62d8fa Drop alias from local DOMAIN import (#144867) 2025-05-14 12:24:46 +02:00
Brett Adams 8ccedd4064 Add credit balance sensor to Teslemetry (#144365)
* Add credits

* Credits string and icon

* Add test

* tests and fixes

* Add units

* Update homeassistant/components/teslemetry/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* Update snapshot with unit

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-14 12:21:45 +02:00
Rob Bierbooms 9a06584a1d Bump influxdb-client to 1.48.0 (#144845)
* Bump influxdb-client to 1.48.0

* Adjust typing, fix mypy

* Update homeassistant/components/influxdb/__init__.py

* Update homeassistant/components/influxdb/__init__.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-14 12:21:26 +02:00
Daniël van den Berg a21e586140 Show Sonos playlists under favorites (#142357)
* Update media_browser.py

* Update favorites.py

* Update media_player.py

* Update media_browser.py

* Update media_player.py

* Update favorites.py

* Update media_browser.py

* Update media_player.py

* Update favorites.py

* Added/fixed testing for showing sonos native playlists in the media browser

* Create a DidlFavorite to wrap playlists.

* Processed PR feedback
2025-05-14 12:14:20 +02:00
epenet 91f01d660f Move ps4 services to separate module (#144870) 2025-05-14 12:04:43 +02:00
peteS-UK 1748dbd60f Add parallel_updates to new updates platform for Squeezebox (#144864)
initial
2025-05-14 11:59:28 +02:00
starkillerOG 5acae7f86d Fix Reolink setup when ONVIF push is unsupported (#144869)
* Fix setup when ONVIF push is not supported

* fix styling
2025-05-14 11:58:29 +02:00
epenet 30ecba9944 Finish cleaning up SamsungTV init tests (#144865)
FInish cleaning up SamsungTV init tests
2025-05-14 11:58:01 +02:00
epenet 4287df5f3d Use HassKey in ps4 (#144868) 2025-05-14 11:51:32 +02:00
hahn-th 063deab3cb Doorbell Event is fired just once in homematicip_cloud (#144357)
* fire event if event type if correct

* Fix requested changes
2025-05-14 11:44:59 +02:00
dependabot[bot] 27798a6004 Bump actions/dependency-review-action from 4.7.0 to 4.7.1 (#144856)
Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4.7.0 to 4.7.1.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](https://github.com/actions/dependency-review-action/compare/v4.7.0...v4.7.1)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: 4.7.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 11:43:14 +02:00
Luke Lashley 577ddd9021 Bump python-snoo to 0.6.6 (#144849) 2025-05-14 11:42:43 +02:00
Allen Porter 34663e160d Bump ical to 9.2.4 (#144852) 2025-05-14 11:42:22 +02:00
Norbert Rittel ac54b81289 Fix spelling of "IP address" in plugwise (#144861) 2025-05-14 11:01:14 +03:00
Penny Wood 67174fb07e Remove myself as code owner of sun component (#144854)
* Remove myself as code owner

I'm haven't actively been working on this for some time. Have removed myself as a code owner.

* cleanup

* cleanup

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-14 09:37:51 +03:00
Brett Adams d2a692393f Fix wall connector states in Teslemetry (#144855)
* Fix wall connector

* Update snapshot
2025-05-14 08:08:24 +02:00
Åke Strandberg 9aa2664188 Change unknown to unknown_code for missing Miele codes to avoid confusion (#144699)
* Change unknown to unknown_code

* Update snapshot

* Automatically replace missing codes with None

* Update snapshot
2025-05-14 08:07:38 +02:00
David Rapan ab5d60e33d Make DHCP discovery aware of the network integration (#144767)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-14 00:59:48 -05:00
epenet 31847d8cfb Adjust handling of SamsungTV misaligned MAC (#144810)
* Cleanup SamsungTV misaligned MAC formatting

* Simplify

* One more

* Revert and add comment

* Adjust comment

* One more
2025-05-14 07:57:33 +02:00
John Hillery 9729f1f38b Provide ability to select nexia RoomIQ sensors (#144278)
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-13 23:16:05 -05:00
peteS-UK 6bc6733c40 Add config flow data descriptions to Squeezebox (#144619)
* add data_descriptions

* tweaks

* review updates
2025-05-13 21:10:47 -07:00
Tsvi Mostovicz b1ffcb4245 Jewish calendar - Fix Parasha values (#144646)
* Fix Parasha values

* Fix test

* Update sensor.py
2025-05-13 21:08:47 -07:00
Josef Zweck f0c5fbfb8a Bump pylamarzocco to 2.0.3 (#144825) 2025-05-13 21:04:38 -07:00
J. Nick Koston c76239806d Bump aioesphomeapi to 31.0.0 (#144778)
* aioesphomeapi update

* Bump aioesphomeapi to 31.0.0

There are some breaking changes with the protobuf naming and types
required some refactoring

changelog: https://github.com/esphome/aioesphomeapi/compare/v30.2.0...v31.0.0

* actually include the commit to bump the lib
2025-05-13 20:39:53 -04:00
Abílio Costa 6d809b0b5a Add service response support to admin services (#144837) 2025-05-13 21:57:15 +01:00
Norbert Rittel de2cbb7f5c Improve user-facing strings of incomfort (#144844)
* Improvements in user-facing strings of `incomfort`

Fix spelling of "IP address" and "timeout"

Remove "temperature" from "Shortcut outside sensor temperature" as this makes no sense and leads to completely wrong translations. This is to indicate an electrical shortcut on the sensor so this should be the last word.

This also matches the naming in the user manual.

* Suggestion from review

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-13 22:47:21 +02:00
Erik Montnemery cd61f37df7 Remove support for condition platforms defining only a CONDITION_SCHEMA (#144832) 2025-05-13 20:53:08 +02:00
epenet 26796f87cd Add device registry snapshots to samsungtv tests (#144804)
* Add device registry snapshots to samsungtv tests

* Simplify

* Adjust

* Reduce
2025-05-13 18:20:43 +02:00
Åke Strandberg e2dd897ac7 Bump dependency pymiele -> 0.5.2 (#144758) 2025-05-13 18:19:49 +02:00
Retha Runolfsson 3bbe4baaf7 Update codeowner for switchbot Integration (#144829)
update codeowners
2025-05-13 18:16:05 +02:00
Alistair Francis d409b86217 Bump automower-ble to 0.2.1 (#144817) 2025-05-13 14:21:56 +01:00
Josef Zweck 7928c15849 Fix blocking call in azure_storage config flow (#144818)
* Fix blocking call in azure_storage config flow

* Fix blocking call in azure_storage config_flow as well

* move session getting to event flow
2025-05-13 14:23:41 +02:00
epenet d197debbc0 Improve SamsungTV config flow type hints (#144820) 2025-05-13 14:02:07 +02:00
Martin Hjelmare 55b9dee448 Fix Z-Wave unique id after controller reset (#144813) 2025-05-13 14:12:00 +03:00
epenet 5c6984d326 Do not abort on invalid host in SamsungTV user flow (#144794) 2025-05-13 10:47:26 +02:00
Josef Zweck a7787d6080 Fix blocking call in azure storage (#144803) 2025-05-13 10:46:46 +02:00
Jeremiah Paige 2db60340c2 Add typing to wsdot (#143117)
* increase wsdot typing

* remove Final types

* help out mypy

* simplify wsdot types

* minor wsdot type changes

* type wsdot state
2025-05-13 10:43:03 +02:00
Mick Vleeshouwer c121631fef Refactor config flow tests to improve result variable usage in Overkiz (#143374)
* Refactor test setup for unique ID migration in Overkiz integration

* Refactor test cases to unify result variable usage in Overkiz config flow tests (resultn -> result)

* Revert change in test_init
2025-05-13 10:35:32 +02:00
epenet b0fb16d48d Remove obsolete compatibility code from SamsungTV (#144800) 2025-05-13 09:54:26 +02:00
Franck Nijhof 3e07f6543e Update debugpy to v1.8.14 (#144755) 2025-05-13 08:14:55 +02:00
Brett Adams d4c2356c70 Create stream on demand in Teslemetry (#144777)
Create stream on demand
2025-05-13 08:05:33 +02:00
epenet eec617b391 Add comments to samsungtv config flow tests (#144787) 2025-05-13 07:54:37 +02:00
Robert Resch c8527fc309 fix hassfest again 2025-02-27 11:11:38 +01:00
Robert Resch 16e69bda9e Fix hassfest 2025-02-27 10:59:12 +01:00
Robert Resch 0713328d7d fix merge 2025-02-27 10:55:19 +01:00
Robert Resch f7fac75693 Merge branch 'dev' into edenhaus-ecovacs-quality-scale 2025-02-27 10:52:59 +01:00
Robert Resch 7bd9508c9e Update homeassistant/components/ecovacs/strings.json 2025-02-27 10:50:38 +01:00
Robert Resch 3d3771732b Evaluate all rules 2024-12-16 15:45:33 +01:00
Robert Resch 877b796a98 Add common data_description 2024-12-16 14:44:42 +01:00
Robert Resch a8f8681844 Fix config-flow rule 2024-12-16 08:39:32 +01:00
Robert Resch 33bb3de1b1 Add quality_scale file to Ecovacs 2024-12-16 08:38:33 +01:00
1179 changed files with 32024 additions and 7453 deletions
+2 -2
View File
@@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -522,7 +522,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
uses: docker/build-push-action@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
+35 -7
View File
@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.7.0
uses: actions/dependency-review-action@v4.7.1
with:
license-check: false # We use our own license audit checks
@@ -944,7 +944,8 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev
libgammu-dev \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1020,6 +1021,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1070,7 +1077,8 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libmariadb-dev-compat
libmariadb-dev-compat \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1154,6 +1162,12 @@ jobs:
steps.pytest-partial.outputs.mariadb }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1202,7 +1216,8 @@ jobs:
sudo apt-get -y install \
bluez \
ffmpeg \
libturbojpeg
libturbojpeg \
libxml2-utils
sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
sudo apt-get -y install \
postgresql-server-dev-14
@@ -1290,6 +1305,12 @@ jobs:
steps.pytest-partial.outputs.postgresql }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1320,7 +1341,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.4.2
uses: codecov/codecov-action@v5.4.3
with:
fail_ci_if_error: true
flags: full-suite
@@ -1357,7 +1378,8 @@ jobs:
bluez \
ffmpeg \
libturbojpeg \
libgammu-dev
libgammu-dev \
libxml2-utils
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
@@ -1436,6 +1458,12 @@ jobs:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
overwrite: true
- name: Beautify test results
# For easier identification of parsing errors
if: needs.info.outputs.skip_coverage != 'true'
run: |
xmllint --format "junit.xml" > "junit.xml-tmp"
mv "junit.xml-tmp" "junit.xml"
- name: Upload test results artifact
if: needs.info.outputs.skip_coverage != 'true' && !cancelled()
uses: actions/upload-artifact@v4.6.2
@@ -1463,7 +1491,7 @@ jobs:
pattern: coverage-*
- name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.4.2
uses: codecov/codecov-action@v5.4.3
with:
fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.17
uses: github/codeql-action/init@v3.28.18
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.17
uses: github/codeql-action/analyze@v3.28.18
with:
category: "/language:python"
+1
View File
@@ -270,6 +270,7 @@ homeassistant.components.image_processing.*
homeassistant.components.image_upload.*
homeassistant.components.imap.*
homeassistant.components.imgw_pib.*
homeassistant.components.immich.*
homeassistant.components.incomfort.*
homeassistant.components.input_button.*
homeassistant.components.input_select.*
Generated
+6 -4
View File
@@ -710,6 +710,8 @@ build.json @home-assistant/supervisor
/tests/components/imeon_inverter/ @Imeon-Energy
/homeassistant/components/imgw_pib/ @bieniu
/tests/components/imgw_pib/ @bieniu
/homeassistant/components/immich/ @mib1185
/tests/components/immich/ @mib1185
/homeassistant/components/improv_ble/ @emontnemery
/tests/components/improv_ble/ @emontnemery
/homeassistant/components/incomfort/ @jbouwh
@@ -1484,8 +1486,8 @@ build.json @home-assistant/supervisor
/tests/components/subaru/ @G-Two
/homeassistant/components/suez_water/ @ooii @jb101010-2
/tests/components/suez_water/ @ooii @jb101010-2
/homeassistant/components/sun/ @Swamp-Ig
/tests/components/sun/ @Swamp-Ig
/homeassistant/components/sun/ @home-assistant/core
/tests/components/sun/ @home-assistant/core
/homeassistant/components/supla/ @mwegrzynek
/homeassistant/components/surepetcare/ @benleb @danielhiversen
/tests/components/surepetcare/ @benleb @danielhiversen
@@ -1498,8 +1500,8 @@ build.json @home-assistant/supervisor
/tests/components/switch_as_x/ @home-assistant/core
/homeassistant/components/switchbee/ @jafar-atili
/tests/components/switchbee/ @jafar-atili
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski
/homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski @zerzhang
/homeassistant/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/tests/components/switchbot_cloud/ @SeraphicRav @laurence-presland @Gigatrappeur
/homeassistant/components/switcher_kis/ @thecode @YogevBokobza
@@ -6,6 +6,7 @@ from typing import Any, Concatenate
from airgradient import AirGradientConnectionError, AirGradientError, get_model_name
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -29,6 +30,7 @@ class AirGradientEntity(CoordinatorEntity[AirGradientCoordinator]):
model_id=measures.model,
serial_number=coordinator.serial_number,
sw_version=measures.firmware_version,
connections={(dr.CONNECTION_NETWORK_MAC, coordinator.serial_number)},
)
+3 -3
View File
@@ -47,7 +47,7 @@ from .const import (
CONF_VIDEO_SOURCE,
DEFAULT_STREAM_PROFILE,
DEFAULT_VIDEO_SOURCE,
DOMAIN as AXIS_DOMAIN,
DOMAIN,
)
from .errors import AuthenticationRequired, CannotConnect
from .hub import AxisHub, get_axis_api
@@ -58,7 +58,7 @@ DEFAULT_PROTOCOL = "https"
PROTOCOL_CHOICES = ["https", "http"]
class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
class AxisFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Axis config flow."""
VERSION = 3
@@ -146,7 +146,7 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
model = self.config[CONF_MODEL]
same_model = [
entry.data[CONF_NAME]
for entry in self.hass.config_entries.async_entries(AXIS_DOMAIN)
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.source != SOURCE_IGNORE and entry.data[CONF_MODEL] == model
]
@@ -39,11 +39,20 @@ async def async_setup_entry(
session = async_create_clientsession(
hass, timeout=ClientTimeout(connect=10, total=12 * 60 * 60)
)
container_client = ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
def create_container_client() -> ContainerClient:
"""Create a ContainerClient."""
return ContainerClient(
account_url=f"https://{entry.data[CONF_ACCOUNT_NAME]}.blob.core.windows.net/",
container_name=entry.data[CONF_CONTAINER_NAME],
credential=entry.data[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=session),
)
# has a blocking call to open in cpython
container_client: ContainerClient = await hass.async_add_executor_job(
create_container_client
)
try:
@@ -27,9 +27,25 @@ _LOGGER = logging.getLogger(__name__)
class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for azure storage."""
def get_account_url(self, account_name: str) -> str:
"""Get the account URL."""
return f"https://{account_name}.blob.core.windows.net/"
async def get_container_client(
self, account_name: str, container_name: str, storage_account_key: str
) -> ContainerClient:
"""Get the container client.
ContainerClient has a blocking call to open in cpython
"""
session = async_get_clientsession(self.hass)
def create_container_client() -> ContainerClient:
return ContainerClient(
account_url=f"https://{account_name}.blob.core.windows.net/",
container_name=container_name,
credential=storage_account_key,
transport=AioHttpTransport(session=session),
)
return await self.hass.async_add_executor_job(create_container_client)
async def validate_config(
self, container_client: ContainerClient
@@ -58,11 +74,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match(
{CONF_ACCOUNT_NAME: user_input[CONF_ACCOUNT_NAME]}
)
container_client = ContainerClient(
account_url=self.get_account_url(user_input[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=user_input[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
@@ -99,12 +114,12 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(reauth_entry.data[CONF_ACCOUNT_NAME]),
container_client = await self.get_container_client(
account_name=reauth_entry.data[CONF_ACCOUNT_NAME],
container_name=reauth_entry.data[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
return self.async_update_reload_and_abort(
@@ -129,13 +144,10 @@ class AzureStorageConfigFlow(ConfigFlow, domain=DOMAIN):
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
container_client = ContainerClient(
account_url=self.get_account_url(
reconfigure_entry.data[CONF_ACCOUNT_NAME]
),
container_client = await self.get_container_client(
account_name=reconfigure_entry.data[CONF_ACCOUNT_NAME],
container_name=user_input[CONF_CONTAINER_NAME],
credential=user_input[CONF_STORAGE_ACCOUNT_KEY],
transport=AioHttpTransport(session=async_get_clientsession(self.hass)),
storage_account_key=user_input[CONF_STORAGE_ACCOUNT_KEY],
)
errors = await self.validate_config(container_client)
if not errors:
@@ -24,7 +24,7 @@ from .const import DOMAIN, EVSE_ID, LOGGER, MODEL_TYPE
type BlueCurrentConfigEntry = ConfigEntry[Connector]
PLATFORMS = [Platform.SENSOR]
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
CHARGE_POINTS = "CHARGE_POINTS"
DATA = "data"
DELAY = 5
@@ -0,0 +1,89 @@
"""Support for Blue Current buttons."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bluecurrent_api.client import Client
from homeassistant.components.button import (
ButtonDeviceClass,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BlueCurrentConfigEntry, Connector
from .entity import ChargepointEntity
@dataclass(kw_only=True, frozen=True)
class ChargePointButtonEntityDescription(ButtonEntityDescription):
"""Describes a Blue Current button entity."""
function: Callable[[Client, str], Coroutine[Any, Any, None]]
CHARGE_POINT_BUTTONS = (
ChargePointButtonEntityDescription(
key="reset",
translation_key="reset",
function=lambda client, evse_id: client.reset(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="reboot",
translation_key="reboot",
function=lambda client, evse_id: client.reboot(evse_id),
device_class=ButtonDeviceClass.RESTART,
),
ChargePointButtonEntityDescription(
key="stop_charge_session",
translation_key="stop_charge_session",
function=lambda client, evse_id: client.stop_session(evse_id),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: BlueCurrentConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Blue Current buttons."""
connector: Connector = entry.runtime_data
async_add_entities(
ChargePointButton(
connector,
button,
evse_id,
)
for evse_id in connector.charge_points
for button in CHARGE_POINT_BUTTONS
)
class ChargePointButton(ChargepointEntity, ButtonEntity):
"""Define a charge point button."""
has_value = True
entity_description: ChargePointButtonEntityDescription
def __init__(
self,
connector: Connector,
description: ChargePointButtonEntityDescription,
evse_id: str,
) -> None:
"""Initialize the button."""
super().__init__(connector, evse_id)
self.entity_description = description
self._attr_unique_id = f"{description.key}_{evse_id}"
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.function(self.connector.client, self.evse_id)
@@ -1,7 +1,5 @@
"""Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -17,12 +15,12 @@ class BlueCurrentEntity(Entity):
_attr_has_entity_name = True
_attr_should_poll = False
has_value = False
def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity."""
self.connector = connector
self.signal = signal
self.has_value = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -43,7 +41,6 @@ class BlueCurrentEntity(Entity):
return self.connector.connected and self.has_value
@callback
@abstractmethod
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
@@ -19,6 +19,17 @@
"current_left": {
"default": "mdi:gauge"
}
},
"button": {
"reset": {
"default": "mdi:restart"
},
"reboot": {
"default": "mdi:restart-alert"
},
"stop_charge_session": {
"default": "mdi:stop"
}
}
}
}
@@ -113,6 +113,17 @@
"grid_max_current": {
"name": "Max grid current"
}
},
"button": {
"stop_charge_session": {
"name": "Stop charge session"
},
"reboot": {
"name": "Reboot"
},
"reset": {
"name": "Reset"
}
}
}
}
@@ -22,13 +22,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.ssl import get_default_context
from .const import (
CONF_GCID,
CONF_READ_ONLY,
CONF_REFRESH_TOKEN,
DOMAIN as BMW_DOMAIN,
SCAN_INTERVALS,
)
from .const import CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, DOMAIN, SCAN_INTERVALS
_LOGGER = logging.getLogger(__name__)
@@ -63,7 +57,7 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
hass,
_LOGGER,
config_entry=config_entry,
name=f"{BMW_DOMAIN}-{config_entry.data[CONF_USERNAME]}",
name=f"{DOMAIN}-{config_entry.data[CONF_USERNAME]}",
update_interval=timedelta(
seconds=SCAN_INTERVALS[config_entry.data[CONF_REGION]]
),
@@ -81,26 +75,26 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]):
except MyBMWCaptchaMissingError as err:
# If a captcha is required (user/password login flow), always trigger the reauth flow
raise ConfigEntryAuthFailed(
translation_domain=BMW_DOMAIN,
translation_domain=DOMAIN,
translation_key="missing_captcha",
) from err
except MyBMWAuthError as err:
# Allow one retry interval before raising AuthFailed to avoid flaky API issues
if self.last_update_success:
raise UpdateFailed(
translation_domain=BMW_DOMAIN,
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
# Clear refresh token and trigger reauth if previous update failed as well
self._update_config_entry_refresh_token(None)
raise ConfigEntryAuthFailed(
translation_domain=BMW_DOMAIN,
translation_domain=DOMAIN,
translation_key="invalid_auth",
) from err
except (MyBMWAPIError, RequestError) as err:
raise UpdateFailed(
translation_domain=BMW_DOMAIN,
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"exception": str(err)},
) from err
@@ -7,15 +7,17 @@ from ssl import SSLError
from bosch_alarm_mode2 import Panel
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
PLATFORMS: list[Platform] = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
]
@@ -52,8 +54,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: BoschAlarmConfigEntry) -
device_registry = dr.async_get(hass)
mac = entry.data.get(CONF_MAC)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
connections={(CONNECTION_NETWORK_MAC, mac)} if mac else set(),
identifiers={(DOMAIN, entry.unique_id or entry.entry_id)},
name=f"Bosch {panel.model}",
manufacturer="Bosch Security Systems",
@@ -34,6 +34,9 @@ async def async_setup_entry(
)
PARALLEL_UPDATES = 0
class AreaAlarmControlPanel(BoschAlarmAreaEntity, AlarmControlPanelEntity):
"""An alarm control panel entity for a bosch alarm panel."""
@@ -0,0 +1,220 @@
"""Support for Bosch Alarm Panel binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_PANEL_FAULTS
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity, BoschAlarmEntity, BoschAlarmPointEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmFaultEntityDescription(BinarySensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
fault: int
FAULT_TYPES = [
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_low",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.BATTERY,
fault=ALARM_PANEL_FAULTS.BATTERY_LOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_battery_mising",
translation_key="panel_fault_battery_mising",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.BATTERY_MISING,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_ac_fail",
translation_key="panel_fault_ac_fail",
entity_registry_enabled_default=True,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.AC_FAIL,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_phone_line_failure",
translation_key="panel_fault_phone_line_failure",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.CONNECTIVITY,
fault=ALARM_PANEL_FAULTS.PHONE_LINE_FAILURE,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_parameter_crc_fail_in_pif",
translation_key="panel_fault_parameter_crc_fail_in_pif",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.PARAMETER_CRC_FAIL_IN_PIF,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_communication_fail_since_rps_hang_up",
translation_key="panel_fault_communication_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.COMMUNICATION_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_sdi_fail_since_rps_hang_up",
translation_key="panel_fault_sdi_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.SDI_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_user_code_tamper_since_rps_hang_up",
translation_key="panel_fault_user_code_tamper_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.USER_CODE_TAMPER_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_fail_to_call_rps_since_rps_hang_up",
translation_key="panel_fault_fail_to_call_rps_since_rps_hang_up",
entity_registry_enabled_default=False,
fault=ALARM_PANEL_FAULTS.FAIL_TO_CALL_RPS_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_point_bus_fail_since_rps_hang_up",
translation_key="panel_fault_point_bus_fail_since_rps_hang_up",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.POINT_BUS_FAIL_SINCE_RPS_HANG_UP,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_overflow",
translation_key="panel_fault_log_overflow",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_OVERFLOW,
),
BoschAlarmFaultEntityDescription(
key="panel_fault_log_threshold",
translation_key="panel_fault_log_threshold",
entity_registry_enabled_default=False,
device_class=BinarySensorDeviceClass.PROBLEM,
fault=ALARM_PANEL_FAULTS.LOG_THRESHOLD,
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up binary sensors for alarm points and the connection status."""
panel = config_entry.runtime_data
entities: list[BinarySensorEntity] = [
PointSensor(panel, point_id, config_entry.unique_id or config_entry.entry_id)
for point_id in panel.points
]
entities.extend(
PanelFaultsSensor(
panel,
config_entry.unique_id or config_entry.entry_id,
fault_type,
)
for fault_type in FAULT_TYPES
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "away"
)
for area_id in panel.areas
)
entities.extend(
AreaReadyToArmSensor(
panel, area_id, config_entry.unique_id or config_entry.entry_id, "home"
)
for area_id in panel.areas
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelFaultsSensor(BoschAlarmEntity, BinarySensorEntity):
"""A binary sensor entity for each fault type in a bosch alarm panel."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: BoschAlarmFaultEntityDescription
def __init__(
self,
panel: Panel,
unique_id: str,
entity_description: BoschAlarmFaultEntityDescription,
) -> None:
"""Set up a binary sensor entity for each fault type in a bosch alarm panel."""
super().__init__(panel, unique_id, True)
self.entity_description = entity_description
self._fault_type = entity_description.fault
self._attr_unique_id = f"{unique_id}_fault_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return if this fault has occurred."""
return self._fault_type in self.panel.panel_faults_ids
class AreaReadyToArmSensor(BoschAlarmAreaEntity, BinarySensorEntity):
"""A binary sensor entity showing if a panel is ready to arm."""
_attr_entity_category = EntityCategory.DIAGNOSTIC
def __init__(
self, panel: Panel, area_id: int, unique_id: str, arm_type: str
) -> None:
"""Set up a binary sensor entity for the arming status in a bosch alarm panel."""
super().__init__(panel, area_id, unique_id, False, False, True)
self.panel = panel
self._arm_type = arm_type
self._attr_translation_key = f"area_ready_to_arm_{arm_type}"
self._attr_unique_id = f"{self._area_unique_id}_ready_to_arm_{arm_type}"
@property
def is_on(self) -> bool:
"""Return if this panel is ready to arm."""
if self._arm_type == "away":
return self._area.all_ready
if self._arm_type == "home":
return self._area.all_ready or self._area.part_ready
return False
class PointSensor(BoschAlarmPointEntity, BinarySensorEntity):
"""A binary sensor entity for a point in a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a binary sensor entity for a point in a bosch alarm panel."""
super().__init__(panel, point_id, unique_id)
self._attr_unique_id = self._point_unique_id
@property
def is_on(self) -> bool:
"""Return if this point sensor is on."""
return self._point.is_open()
@@ -6,12 +6,13 @@ import asyncio
from collections.abc import Mapping
import logging
import ssl
from typing import Any
from typing import Any, Self
from bosch_alarm_mode2 import Panel
import voluptuous as vol
from homeassistant.config_entries import (
SOURCE_DHCP,
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlow,
@@ -20,11 +21,14 @@ from homeassistant.config_entries import (
from homeassistant.const import (
CONF_CODE,
CONF_HOST,
CONF_MAC,
CONF_MODEL,
CONF_PASSWORD,
CONF_PORT,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import CONF_INSTALLER_CODE, CONF_USER_CODE, DOMAIN
@@ -88,6 +92,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
"""Init config flow."""
self._data: dict[str, Any] = {}
self.mac: str | None = None
self.host: str | None = None
def is_matching(self, other_flow: Self) -> bool:
"""Return True if other_flow is matching this flow."""
return self.mac == other_flow.mac or self.host == other_flow.host
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -96,9 +106,12 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
if user_input is not None:
self.host = user_input[CONF_HOST]
if self.source == SOURCE_USER:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, serial) = await try_connect(user_input, 0)
(model, _) = await try_connect(user_input, 0)
except (
OSError,
ConnectionRefusedError,
@@ -129,6 +142,55 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
self.mac = format_mac(discovery_info.macaddress)
self.host = discovery_info.ip
if self.hass.config_entries.flow.async_has_matching_flow(self):
return self.async_abort(reason="already_in_progress")
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data[CONF_MAC] == self.mac:
result = self.hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_HOST: discovery_info.ip,
},
)
if result:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
return self.async_abort(reason="already_configured")
try:
# Use load_selector = 0 to fetch the panel model without authentication.
(model, _) = await try_connect(
{CONF_HOST: discovery_info.ip, CONF_PORT: 7700}, 0
)
except (
OSError,
ConnectionRefusedError,
ssl.SSLError,
asyncio.exceptions.TimeoutError,
):
return self.async_abort(reason="cannot_connect")
except Exception:
_LOGGER.exception("Unexpected exception")
return self.async_abort(reason="unknown")
self.context["title_placeholders"] = {
"model": model,
"host": discovery_info.ip,
}
self._data = {
CONF_HOST: discovery_info.ip,
CONF_MAC: self.mac,
CONF_MODEL: model,
CONF_PORT: 7700,
}
return await self.async_step_auth()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -172,7 +234,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if serial_number:
await self.async_set_unique_id(str(serial_number))
if self.source == SOURCE_USER:
if self.source in (SOURCE_USER, SOURCE_DHCP):
if serial_number:
self._abort_if_unique_id_configured()
else:
@@ -184,6 +246,7 @@ class BoschAlarmConfigFlow(ConfigFlow, domain=DOMAIN):
)
if serial_number:
self._abort_if_unique_id_mismatch(reason="device_mismatch")
return self.async_update_reload_and_abort(
self._get_reconfigure_entry(),
data=self._data,
+36 -1
View File
@@ -17,9 +17,13 @@ class BoschAlarmEntity(Entity):
_attr_has_entity_name = True
def __init__(self, panel: Panel, unique_id: str) -> None:
def __init__(
self, panel: Panel, unique_id: str, observe_faults: bool = False
) -> None:
"""Set up a entity for a bosch alarm panel."""
self.panel = panel
self._observe_faults = observe_faults
self._attr_should_poll = False
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=f"Bosch {panel.model}",
@@ -34,10 +38,14 @@ class BoschAlarmEntity(Entity):
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
self.panel.connection_status_observer.attach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
self.panel.connection_status_observer.detach(self.schedule_update_ha_state)
if self._observe_faults:
self.panel.faults_observer.attach(self.schedule_update_ha_state)
class BoschAlarmAreaEntity(BoschAlarmEntity):
@@ -88,6 +96,33 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
self._area.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmPointEntity(BoschAlarmEntity):
"""A base entity for point related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, point_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._point_id = point_id
self._point_unique_id = f"{unique_id}_point_{point_id}"
self._point = panel.points[point_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._point_unique_id)},
name=self._point.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._point.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._point.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
@@ -1,6 +1,15 @@
{
"entity": {
"sensor": {
"alarms_gas": {
"default": "mdi:alert-circle"
},
"alarms_fire": {
"default": "mdi:alert-circle"
},
"alarms_burglary": {
"default": "mdi:alert-circle"
},
"faulting_points": {
"default": "mdi:alert-circle"
}
@@ -24,6 +33,44 @@
"on": "mdi:lock-open"
}
}
},
"binary_sensor": {
"panel_fault_parameter_crc_fail_in_pif": {
"default": "mdi:alert-circle"
},
"panel_fault_phone_line_failure": {
"default": "mdi:alert-circle"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"default": "mdi:alert-circle"
},
"panel_fault_log_overflow": {
"default": "mdi:alert-circle"
},
"panel_fault_log_threshold": {
"default": "mdi:alert-circle"
},
"area_ready_to_arm_away": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-lock"
}
},
"area_ready_to_arm_home": {
"default": "mdi:shield",
"state": {
"on": "mdi:shield-home"
}
}
}
}
}
@@ -3,6 +3,11 @@
"name": "Bosch Alarm",
"codeowners": ["@mag1024", "@sanjay900"],
"config_flow": true,
"dhcp": [
{
"macaddress": "000463*"
}
],
"documentation": "https://www.home-assistant.io/integrations/bosch_alarm",
"integration_type": "device",
"iot_class": "local_push",
@@ -39,15 +39,15 @@ rules:
entity-unavailable: todo
integration-owner: done
log-when-unavailable: todo
parallel-updates: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
+38 -2
View File
@@ -6,6 +6,7 @@ from collections.abc import Callable
from dataclasses import dataclass
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.const import ALARM_MEMORY_PRIORITIES
from bosch_alarm_mode2.panel import Area
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
@@ -15,18 +16,53 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .entity import BoschAlarmAreaEntity
ALARM_TYPES = {
"burglary": {
ALARM_MEMORY_PRIORITIES.BURGLARY_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.BURGLARY_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.BURGLARY_ALARM: "alarm",
},
"gas": {
ALARM_MEMORY_PRIORITIES.GAS_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.GAS_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.GAS_ALARM: "alarm",
},
"fire": {
ALARM_MEMORY_PRIORITIES.FIRE_SUPERVISORY: "supervisory",
ALARM_MEMORY_PRIORITIES.FIRE_TROUBLE: "trouble",
ALARM_MEMORY_PRIORITIES.FIRE_ALARM: "alarm",
},
}
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSensorEntityDescription(SensorEntityDescription):
"""Describes Bosch Alarm sensor entity."""
value_fn: Callable[[Area], int]
value_fn: Callable[[Area], str | int]
observe_alarms: bool = False
observe_ready: bool = False
observe_status: bool = False
def priority_value_fn(priority_info: dict[int, str]) -> Callable[[Area], str]:
"""Build a value_fn for a given priority type."""
return lambda area: next(
(key for priority, key in priority_info.items() if priority in area.alarms_ids),
"no_issues",
)
SENSOR_TYPES: list[BoschAlarmSensorEntityDescription] = [
*[
BoschAlarmSensorEntityDescription(
key=f"alarms_{key}",
translation_key=f"alarms_{key}",
value_fn=priority_value_fn(priority_type),
observe_alarms=True,
)
for key, priority_type in ALARM_TYPES.items()
],
BoschAlarmSensorEntityDescription(
key="faulting_points",
translation_key="faulting_points",
@@ -81,6 +117,6 @@ class BoschAreaSensor(BoschAlarmAreaEntity, SensorEntity):
self._attr_unique_id = f"{self._area_unique_id}_{entity_description.key}"
@property
def native_value(self) -> int:
def native_value(self) -> str | int:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self._area)
@@ -1,5 +1,6 @@
{
"config": {
"flow_title": "{model} ({host})",
"step": {
"user": {
"data": {
@@ -42,6 +43,7 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"abort": {
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
@@ -56,22 +58,95 @@
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is being cycled."
"message": "Door cannot be manipulated while it is momentarily unlocked."
}
},
"entity": {
"binary_sensor": {
"panel_fault_battery_mising": {
"name": "Battery missing"
},
"panel_fault_ac_fail": {
"name": "AC Failure"
},
"panel_fault_parameter_crc_fail_in_pif": {
"name": "CRC failure in panel configuration"
},
"panel_fault_phone_line_failure": {
"name": "Phone line failure"
},
"panel_fault_sdi_fail_since_rps_hang_up": {
"name": "SDI failure since RPS hang up"
},
"panel_fault_user_code_tamper_since_rps_hang_up": {
"name": "User code tamper since RPS hang up"
},
"panel_fault_fail_to_call_rps_since_rps_hang_up": {
"name": "Failure to call RPS since RPS hang up"
},
"panel_fault_point_bus_fail_since_rps_hang_up": {
"name": "Point bus failure since RPS hang up"
},
"panel_fault_log_overflow": {
"name": "Log overflow"
},
"panel_fault_log_threshold": {
"name": "Log threshold reached"
},
"area_ready_to_arm_away": {
"name": "Area ready to arm away",
"state": {
"on": "Ready",
"off": "Not ready"
}
},
"area_ready_to_arm_home": {
"name": "Area ready to arm home",
"state": {
"on": "Ready",
"off": "Not ready"
}
}
},
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Cycling"
"name": "Momentarily unlocked"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"alarms_gas": {
"name": "Gas alarm issues",
"state": {
"supervisory": "Supervisory",
"trouble": "Trouble",
"alarm": "Alarm",
"no_issues": "No issues"
}
},
"alarms_fire": {
"name": "Fire alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"alarms_burglary": {
"name": "Burglary alarm issues",
"state": {
"supervisory": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::supervisory%]",
"trouble": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::trouble%]",
"alarm": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::alarm%]",
"no_issues": "[%key:component::bosch_alarm::entity::sensor::alarms_gas::state::no_issues%]"
}
},
"faulting_points": {
"name": "Faulting points",
"unit_of_measurement": "points"
@@ -60,7 +60,7 @@ from .const import (
ADDED_CAST_DEVICES_KEY,
CAST_MULTIZONE_MANAGER_KEY,
CONF_IGNORE_CEC,
DOMAIN as CAST_DOMAIN,
DOMAIN,
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
@@ -315,7 +315,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self._cast_view_remove_handler: CALLBACK_TYPE | None = None
self._attr_unique_id = str(cast_info.uuid)
self._attr_device_info = DeviceInfo(
identifiers={(CAST_DOMAIN, str(cast_info.uuid).replace("-", ""))},
identifiers={(DOMAIN, str(cast_info.uuid).replace("-", ""))},
manufacturer=str(cast_info.cast_info.manufacturer),
model=cast_info.cast_info.model_name,
name=str(cast_info.friendly_name),
@@ -591,7 +591,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
"""Generate root node."""
children = []
# Add media browsers
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
children.extend(
await platform.async_get_media_browser_root_object(
self.hass, self._chromecast.cast_type
@@ -650,7 +650,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
platform: CastProtocol
assert media_content_type is not None
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
browse_media = await platform.async_browse_media(
self.hass,
media_content_type,
@@ -680,7 +680,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
# Handle media supported by a known cast app
if media_type == CAST_DOMAIN:
if media_type == DOMAIN:
try:
app_data = json.loads(media_id)
if metadata := extra.get("metadata"):
@@ -712,7 +712,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return
# Try the cast platforms
for platform in self.hass.data[CAST_DOMAIN]["cast_platform"].values():
for platform in self.hass.data[DOMAIN]["cast_platform"].values():
result = await platform.async_play_media(
self.hass, self.entity_id, chromecast, media_type, media_id
)
@@ -43,7 +43,7 @@ from homeassistant.util.dt import utcnow
from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DOMAIN as CLOUD_DOMAIN,
DOMAIN,
PREF_ALEXA_REPORT_STATE,
PREF_ENABLE_ALEXA,
PREF_SHOULD_EXPOSE,
@@ -55,7 +55,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_ALEXA = f"{CLOUD_DOMAIN}.{ALEXA_DOMAIN}"
CLOUD_ALEXA = f"{DOMAIN}.{ALEXA_DOMAIN}"
# Time to wait when entity preferences have changed before syncing it to
# the cloud.
@@ -41,7 +41,7 @@ from .const import (
CONF_ENTITY_CONFIG,
CONF_FILTER,
DEFAULT_DISABLE_2FA,
DOMAIN as CLOUD_DOMAIN,
DOMAIN,
PREF_DISABLE_2FA,
PREF_SHOULD_EXPOSE,
)
@@ -52,7 +52,7 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
CLOUD_GOOGLE = f"{CLOUD_DOMAIN}.{GOOGLE_DOMAIN}"
CLOUD_GOOGLE = f"{DOMAIN}.{GOOGLE_DOMAIN}"
SUPPORTED_DOMAINS = {
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.100.0"],
"requirements": ["hass-nabucasa==0.101.0"],
"single_config_entry": true
}
+1 -3
View File
@@ -134,11 +134,9 @@ class ComelitClimateEntity(ComelitBridgeBaseEntity, ClimateEntity):
self._attr_current_temperature = values[0] / 10
self._attr_hvac_action = None
if _mode == ClimaComelitMode.OFF:
self._attr_hvac_action = HVACAction.OFF
if not _active:
self._attr_hvac_action = HVACAction.IDLE
if _mode in API_STATUS:
elif _mode in API_STATUS:
self._attr_hvac_action = API_STATUS[_mode]["hvac_action"]
self._attr_hvac_mode = None
+2 -8
View File
@@ -7,7 +7,7 @@ from typing import Any, cast
from aiocomelit import ComelitSerialBridgeObject
from aiocomelit.const import COVER, STATE_COVER, STATE_OFF, STATE_ON
from homeassistant.components.cover import CoverDeviceClass, CoverEntity, CoverState
from homeassistant.components.cover import CoverDeviceClass, CoverEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
@@ -68,16 +68,10 @@ class ComelitCoverEntity(ComelitBridgeBaseEntity, RestoreEntity, CoverEntity):
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if self._last_state in [None, "unknown"]:
return None
if self.device_status != STATE_COVER.index("stopped"):
return False
if self._last_action:
return self._last_action == STATE_COVER.index("closing")
return self._last_state == CoverState.CLOSED
return None
@property
def is_closing(self) -> bool:
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "bronze",
"requirements": ["aiocomelit==0.12.1"]
"requirements": ["aiocomelit==0.12.3"]
}
@@ -55,10 +55,8 @@ rules:
docs-known-limitations:
status: exempt
comment: no known limitations, yet
docs-supported-devices:
status: todo
comment: review and complete missing ones
docs-supported-functions: todo
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
@@ -6,5 +6,5 @@
"integration_type": "service",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["debugpy==1.8.13"]
"requirements": ["debugpy==1.8.14"]
}
+2 -7
View File
@@ -17,12 +17,7 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_send
from ..const import (
CONF_MASTER_GATEWAY,
DOMAIN as DECONZ_DOMAIN,
HASSIO_CONFIGURATION_URL,
PLATFORMS,
)
from ..const import CONF_MASTER_GATEWAY, DOMAIN, HASSIO_CONFIGURATION_URL, PLATFORMS
from .config import DeconzConfig
if TYPE_CHECKING:
@@ -193,7 +188,7 @@ class DeconzHub:
config_entry_id=self.config_entry.entry_id,
configuration_url=configuration_url,
entry_type=dr.DeviceEntryType.SERVICE,
identifiers={(DECONZ_DOMAIN, self.api.config.bridge_id)},
identifiers={(DOMAIN, self.api.config.bridge_id)},
manufacturer="Dresden Elektronik",
model=self.api.config.model_id,
name=self.api.config.name,
@@ -6,12 +6,16 @@ from datetime import datetime
from typing import Any
from homeassistant.components.media_player import (
BrowseMedia,
MediaClass,
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
RepeatMode,
SearchMedia,
SearchMediaQuery,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -407,3 +411,18 @@ class DemoSearchPlayer(AbstractDemoPlayer):
"""A Demo media player that supports searching."""
_attr_supported_features = SEARCH_PLAYER_SUPPORT
async def async_search_media(self, query: SearchMediaQuery) -> SearchMedia:
"""Demo implementation of search media."""
return SearchMedia(
result=[
BrowseMedia(
title="Search result",
media_class=MediaClass.MOVIE,
media_content_type=MediaType.MOVIE,
media_content_id="search_result_id",
can_play=True,
can_expand=False,
)
]
)
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.0"],
"requirements": ["denonavr==1.1.1"],
"ssdp": [
{
"manufacturer": "Denon",
@@ -8,11 +8,7 @@ import voluptuous as vol
from homeassistant.const import CONF_DOMAIN
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
TriggerProtocol,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from . import (
@@ -25,13 +21,28 @@ from .helpers import async_validate_device_automation_config
TRIGGER_SCHEMA = DEVICE_TRIGGER_BASE_SCHEMA.extend({}, extra=vol.ALLOW_EXTRA)
class DeviceAutomationTriggerProtocol(TriggerProtocol, Protocol):
class DeviceAutomationTriggerProtocol(Protocol):
"""Define the format of device_trigger modules.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config
from TriggerProtocol.
Each module must define either TRIGGER_SCHEMA or async_validate_trigger_config.
"""
TRIGGER_SCHEMA: vol.Schema
async def async_validate_trigger_config(
self, hass: HomeAssistant, config: ConfigType
) -> ConfigType:
"""Validate config."""
async def async_attach_trigger(
self,
hass: HomeAssistant,
config: ConfigType,
action: TriggerActionType,
trigger_info: TriggerInfo,
) -> CALLBACK_TYPE:
"""Attach a trigger."""
async def async_get_trigger_capabilities(
self, hass: HomeAssistant, config: ConfigType
) -> dict[str, vol.Schema]:
+27 -1
View File
@@ -7,6 +7,7 @@ from collections.abc import Callable
from datetime import timedelta
from fnmatch import translate
from functools import lru_cache, partial
from ipaddress import IPv4Address
import itertools
import logging
import re
@@ -22,6 +23,7 @@ from aiodiscover.discovery import (
from cached_ipaddress import cached_ip_addresses
from homeassistant import config_entries
from homeassistant.components import network
from homeassistant.components.device_tracker import (
ATTR_HOST_NAME,
ATTR_IP,
@@ -421,9 +423,33 @@ class DHCPWatcher(WatcherBase):
response.ip_address, response.hostname, response.mac_address
)
async def async_get_adapter_indexes(self) -> list[int] | None:
"""Get the adapter indexes."""
adapters = await network.async_get_adapters(self.hass)
if network.async_only_default_interface_enabled(adapters):
return None
return [
adapter["index"]
for adapter in adapters
if (
adapter["enabled"]
and adapter["index"] is not None
and adapter["ipv4"]
and (
addresses := [IPv4Address(ip["address"]) for ip in adapter["ipv4"]]
)
and any(
ip for ip in addresses if not ip.is_loopback and not ip.is_global
)
)
]
async def async_start(self) -> None:
"""Start watching for dhcp packets."""
self._unsub = await aiodhcpwatcher.async_start(self._async_process_dhcp_request)
self._unsub = await aiodhcpwatcher.async_start(
self._async_process_dhcp_request,
await self.async_get_adapter_indexes(),
)
class RediscoveryWatcher(WatcherBase):
@@ -2,6 +2,7 @@
"domain": "dhcp",
"name": "DHCP Discovery",
"codeowners": ["@bdraco"],
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/dhcp",
"integration_type": "system",
"iot_class": "local_push",
+4 -144
View File
@@ -2,32 +2,13 @@
from __future__ import annotations
from http import HTTPStatus
import os
import re
import threading
import requests
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from homeassistant.core import HomeAssistant
from .const import (
_LOGGER,
ATTR_FILENAME,
ATTR_OVERWRITE,
ATTR_SUBDIR,
ATTR_URL,
CONF_DOWNLOAD_DIR,
DOMAIN,
DOWNLOAD_COMPLETED_EVENT,
DOWNLOAD_FAILED_EVENT,
SERVICE_DOWNLOAD_FILE,
)
from .const import _LOGGER, CONF_DOWNLOAD_DIR
from .services import register_services
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@@ -44,127 +25,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
return False
def download_file(service: ServiceCall) -> None:
"""Start thread to download file specified in the URL."""
def do_download() -> None:
"""Download the file."""
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:
_LOGGER.warning(
"Downloading '%s' failed, status_code=%d", url, req.status_code
)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
else:
if filename is None and "content-disposition" in req.headers:
match = re.findall(
r"filename=(\S+)", req.headers["content-disposition"]
)
if match:
filename = match[0].strip("'\" ")
if not filename:
filename = os.path.basename(url).strip()
if not filename:
filename = "ha_download"
# Check the filename
raise_if_invalid_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
subdir_path = os.path.join(download_path, subdir)
# Ensure subdir exist
os.makedirs(subdir_path, exist_ok=True)
final_path = os.path.join(subdir_path, filename)
else:
final_path = os.path.join(download_path, filename)
path, ext = os.path.splitext(final_path)
# If file exist append a number.
# We test filename, filename_2..
if not overwrite:
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
final_path = f"{path}_{tries}.{ext}"
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, "wb") as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
{"url": url, "filename": filename},
)
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
except ValueError:
_LOGGER.exception("Invalid value")
hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
threading.Thread(target=do_download).start()
async_register_admin_service(
hass,
DOMAIN,
SERVICE_DOWNLOAD_FILE,
download_file,
schema=vol.Schema(
{
vol.Optional(ATTR_FILENAME): cv.string,
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
}
),
)
register_services(hass)
return True
@@ -0,0 +1,159 @@
"""Support for functionality to download files."""
from __future__ import annotations
from http import HTTPStatus
import os
import re
import threading
import requests
import voluptuous as vol
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path
from .const import (
_LOGGER,
ATTR_FILENAME,
ATTR_OVERWRITE,
ATTR_SUBDIR,
ATTR_URL,
CONF_DOWNLOAD_DIR,
DOMAIN,
DOWNLOAD_COMPLETED_EVENT,
DOWNLOAD_FAILED_EVENT,
SERVICE_DOWNLOAD_FILE,
)
def download_file(service: ServiceCall) -> None:
"""Start thread to download file specified in the URL."""
entry = service.hass.config_entries.async_loaded_entries(DOMAIN)[0]
download_path = entry.data[CONF_DOWNLOAD_DIR]
def do_download() -> None:
"""Download the file."""
try:
url = service.data[ATTR_URL]
subdir = service.data.get(ATTR_SUBDIR)
filename = service.data.get(ATTR_FILENAME)
overwrite = service.data.get(ATTR_OVERWRITE)
if subdir:
# Check the path
raise_if_invalid_path(subdir)
final_path = None
req = requests.get(url, stream=True, timeout=10)
if req.status_code != HTTPStatus.OK:
_LOGGER.warning(
"Downloading '%s' failed, status_code=%d", url, req.status_code
)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
else:
if filename is None and "content-disposition" in req.headers:
match = re.findall(
r"filename=(\S+)", req.headers["content-disposition"]
)
if match:
filename = match[0].strip("'\" ")
if not filename:
filename = os.path.basename(url).strip()
if not filename:
filename = "ha_download"
# Check the filename
raise_if_invalid_filename(filename)
# Do we want to download to subdir, create if needed
if subdir:
subdir_path = os.path.join(download_path, subdir)
# Ensure subdir exist
os.makedirs(subdir_path, exist_ok=True)
final_path = os.path.join(subdir_path, filename)
else:
final_path = os.path.join(download_path, filename)
path, ext = os.path.splitext(final_path)
# If file exist append a number.
# We test filename, filename_2..
if not overwrite:
tries = 1
final_path = path + ext
while os.path.isfile(final_path):
tries += 1
final_path = f"{path}_{tries}.{ext}"
_LOGGER.debug("%s -> %s", url, final_path)
with open(final_path, "wb") as fil:
for chunk in req.iter_content(1024):
fil.write(chunk)
_LOGGER.debug("Downloading of %s done", url)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_COMPLETED_EVENT}",
{"url": url, "filename": filename},
)
except requests.exceptions.ConnectionError:
_LOGGER.exception("ConnectionError occurred for %s", url)
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
except ValueError:
_LOGGER.exception("Invalid value")
service.hass.bus.fire(
f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}",
{"url": url, "filename": filename},
)
# Remove file if we started downloading but failed
if final_path and os.path.isfile(final_path):
os.remove(final_path)
threading.Thread(target=do_download).start()
def register_services(hass: HomeAssistant) -> None:
"""Register the services for the downloader component."""
async_register_admin_service(
hass,
DOMAIN,
SERVICE_DOWNLOAD_FILE,
download_file,
schema=vol.Schema(
{
vol.Optional(ATTR_FILENAME): cv.string,
vol.Optional(ATTR_SUBDIR): cv.string,
vol.Required(ATTR_URL): cv.url,
vol.Optional(ATTR_OVERWRITE, default=False): cv.boolean,
}
),
)
@@ -148,11 +148,6 @@ class EcoNetThermostat(EcoNetEntity[Thermostat], ClimateEntity):
if target_temp_low or target_temp_high:
self._econet.set_set_point(None, target_temp_high, target_temp_low)
@property
def is_aux_heat(self) -> bool:
"""Return true if aux heater."""
return self._econet.mode == ThermostatOperationMode.EMERGENCY_HEAT
@property
def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool, mode.
@@ -17,7 +17,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .util import get_supported_entitites
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -49,7 +49,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
async_add_entities(
get_supported_entitites(
get_supported_entities(
config_entry.runtime_data, EcovacsBinarySensor, ENTITY_DESCRIPTIONS
)
)
+4 -4
View File
@@ -16,13 +16,13 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .const import SUPPORTED_LIFESPANS, SUPPORTED_STATION_ACTIONS
from .const import SUPPORTED_LIFESPANS
from .entity import (
EcovacsCapabilityEntityDescription,
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_supported_entitites
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -62,7 +62,7 @@ STATION_ENTITY_DESCRIPTIONS = tuple(
key=f"station_action_{action.name.lower()}",
translation_key=f"station_action_{action.name.lower()}",
)
for action in SUPPORTED_STATION_ACTIONS
for action in StationAction
)
@@ -85,7 +85,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entitites(
entities: list[EcovacsEntity] = get_supported_entities(
controller, EcovacsButtonEntity, ENTITY_DESCRIPTIONS
)
entities.extend(
@@ -172,7 +172,13 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input:
self._async_abort_entries_match({CONF_USERNAME: user_input[CONF_USERNAME]})
self._async_abort_entries_match(
{
CONF_USERNAME: user_input[CONF_USERNAME],
CONF_OVERRIDE_REST_URL: user_input.get(CONF_OVERRIDE_REST_URL),
CONF_OVERRIDE_MQTT_URL: user_input.get(CONF_OVERRIDE_MQTT_URL),
}
)
errors = await _validate_input(self.hass, user_input)
@@ -214,6 +220,9 @@ class EcovacsConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=self.add_suggested_values_to_schema(
data_schema=vol.Schema(schema), suggested_values=user_input
),
description_placeholders={
"account_name": "Ecovacs",
},
errors=errors,
last_step=True,
)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==13.1.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==13.2.0"]
}
+2 -2
View File
@@ -25,7 +25,7 @@ from .entity import (
EcovacsEntity,
EventT,
)
from .util import get_supported_entitites
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -87,7 +87,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entitites(
entities: list[EcovacsEntity] = get_supported_entities(
controller, EcovacsNumberEntity, ENTITY_DESCRIPTIONS
)
if entities:
@@ -0,0 +1,95 @@
rules:
# Bronze
config-flow: done
test-before-configure: done
unique-config-entry: done
config-flow-test-coverage: done
runtime-data: done
test-before-setup:
status: todo
comment: Legacy code will not raise on setup currently
appropriate-polling:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client pulls only once at beginning and afterwards is pushed based
entity-unique-id: done
has-entity-name: done
entity-event-setup: done
dependency-transparency:
status: todo
comment: Currently unsure if all dependencies need to validated or only direct ones.
action-setup:
status: done
comment: "`raw_get_positions` is a entity service"
common-modules: done
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
docs-actions: todo
brands: done
# Silver
config-entry-unloading: done
log-when-unavailable: todo
entity-unavailable: done
action-exceptions: todo
reauthentication-flow: todo
parallel-updates:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client uses internally semaphores to prevent to many parallel requests
test-coverage: todo
integration-owner: done
docs-installation-parameters: todo
docs-configuration-parameters: todo
# Gold
entity-translations:
status: todo
comment: |
@mib1185 Legacy entities are not translated
entity-device-class: done
devices: done
entity-category: done
entity-disabled-by-default: done
discovery:
status: exempt
comment: Not supported as we don't talk directly to the devices
stale-devices: todo
diagnostics: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
dynamic-devices:
status: todo
comment: New devices are discovered only on boot currently
discovery-update-info:
status: exempt
comment: Not supported as we don't talk directly to the devices
repair-issues: todo
docs-use-cases: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-data-update: todo
docs-known-limitations: todo
docs-troubleshooting: todo
docs-examples: todo
# Platinum
async-dependency:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client is async
inject-websession:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client uses the passed websession
strict-typing:
status: todo
comment: |
@mib1185 Please check legacy code.
deebot-client is typed
+2 -2
View File
@@ -16,7 +16,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import EcovacsConfigEntry
from .entity import EcovacsCapabilityEntityDescription, EcovacsDescriptionEntity, EventT
from .util import get_name_key, get_supported_entitites
from .util import get_name_key, get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -59,7 +59,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities = get_supported_entitites(
entities = get_supported_entities(
controller, EcovacsSelectEntity, ENTITY_DESCRIPTIONS
)
if entities:
+41 -6
View File
@@ -6,7 +6,8 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan
from deebot_client.capabilities import CapabilityEvent, CapabilityLifeSpan, DeviceType
from deebot_client.device import Device
from deebot_client.events import (
BatteryEvent,
ErrorEvent,
@@ -34,7 +35,7 @@ from homeassistant.const import (
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -47,7 +48,7 @@ from .entity import (
EcovacsLegacyEntity,
EventT,
)
from .util import get_name_key, get_options, get_supported_entitites
from .util import get_name_key, get_options, get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -59,6 +60,15 @@ class EcovacsSensorEntityDescription(
"""Ecovacs sensor entity description."""
value_fn: Callable[[EventT], StateType]
native_unit_of_measurement_fn: Callable[[DeviceType], str | None] | None = None
@callback
def get_area_native_unit_of_measurement(device_type: DeviceType) -> str | None:
"""Get the area native unit of measurement based on device type."""
if device_type is DeviceType.MOWER:
return UnitOfArea.SQUARE_CENTIMETERS
return UnitOfArea.SQUARE_METERS
ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
@@ -68,7 +78,9 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
capability_fn=lambda caps: caps.stats.clean,
value_fn=lambda e: e.area,
translation_key="stats_area",
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@@ -85,8 +97,10 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
key="total_stats_area",
translation_key="total_stats_area",
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
device_class=SensorDeviceClass.AREA,
native_unit_of_measurement_fn=get_area_native_unit_of_measurement,
state_class=SensorStateClass.TOTAL_INCREASING,
suggested_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[TotalStatsEvent](
capability_fn=lambda caps: caps.stats.total,
@@ -197,7 +211,7 @@ async def async_setup_entry(
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entitites(
entities: list[EcovacsEntity] = get_supported_entities(
controller, EcovacsSensor, ENTITY_DESCRIPTIONS
)
entities.extend(
@@ -249,6 +263,27 @@ class EcovacsSensor(
entity_description: EcovacsSensorEntityDescription
def __init__(
self,
device: Device,
capability: CapabilityEvent,
entity_description: EcovacsSensorEntityDescription,
**kwargs: Any,
) -> None:
"""Initialize entity."""
super().__init__(device, capability, entity_description, **kwargs)
if (
entity_description.native_unit_of_measurement_fn
and (
native_unit_of_measurement
:= entity_description.native_unit_of_measurement_fn(
device.capabilities.device_type
)
)
is not None
):
self._attr_native_unit_of_measurement = native_unit_of_measurement
async def async_added_to_hass(self) -> None:
"""Set up the event listeners now that hass is ready."""
await super().async_added_to_hass()
@@ -22,8 +22,12 @@
"verify_mqtt_certificate": "Verify MQTT SSL certificate"
},
"data_description": {
"country": "The country of your {account_name} account.",
"override_rest_url": "Enter the REST URL of your self-hosted instance including the scheme (http/https).",
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts)."
"override_mqtt_url": "Enter the MQTT URL of your self-hosted instance including the scheme (mqtt/mqtts).",
"password": "[%key:common::config_flow::data_description::password%]",
"username": "[%key:common::config_flow::data_description::username%]",
"verify_mqtt_certificate": "Should SSL certificates be verified? Uncheck this checkbox only if you are using a self-signed certificate."
}
},
"user": {
+2 -2
View File
@@ -17,7 +17,7 @@ from .entity import (
EcovacsDescriptionEntity,
EcovacsEntity,
)
from .util import get_supported_entitites
from .util import get_supported_entities
@dataclass(kw_only=True, frozen=True)
@@ -109,7 +109,7 @@ async def async_setup_entry(
) -> None:
"""Add entities for passed config_entry in HA."""
controller = config_entry.runtime_data
entities: list[EcovacsEntity] = get_supported_entitites(
entities: list[EcovacsEntity] = get_supported_entities(
controller, EcovacsSwitchEntity, ENTITY_DESCRIPTIONS
)
if entities:
+1 -1
View File
@@ -32,7 +32,7 @@ def get_client_device_id(hass: HomeAssistant, self_hosted: bool) -> str:
)
def get_supported_entitites(
def get_supported_entities(
controller: EcovacsController,
entity_class: type[EcovacsDescriptionEntity],
descriptions: tuple[EcovacsCapabilityEntityDescription, ...],
@@ -13,6 +13,7 @@ PLATFORMS = [
Platform.CLIMATE,
Platform.LIGHT,
Platform.NUMBER,
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TIME,
@@ -15,6 +15,12 @@
},
"night_temperature_offset": {
"default": "mdi:thermometer"
},
"system_led": {
"default": "mdi:led-on",
"state": {
"0": "mdi:led-off"
}
}
},
"sensor": {
@@ -109,6 +109,20 @@ HEATER_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalHeater], ..
),
)
GENERAL_DESCRIPTIONS: tuple[EheimDigitalNumberDescription[EheimDigitalDevice], ...] = (
EheimDigitalNumberDescription[EheimDigitalDevice](
key="system_led",
translation_key="system_led",
entity_category=EntityCategory.CONFIG,
native_min_value=0,
native_max_value=100,
native_step=PRECISION_WHOLE,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.sys_led,
set_value_fn=lambda device, value: device.set_sys_led(int(value)),
),
)
async def async_setup_entry(
hass: HomeAssistant,
@@ -138,6 +152,10 @@ async def async_setup_entry(
)
for description in HEATER_DESCRIPTIONS
)
entities.extend(
EheimDigitalNumber[EheimDigitalDevice](coordinator, device, description)
for description in GENERAL_DESCRIPTIONS
)
async_add_entities(entities)
@@ -0,0 +1,102 @@
"""EHEIM Digital select entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Generic, TypeVar, override
from eheimdigital.classic_vario import EheimDigitalClassicVario
from eheimdigital.device import EheimDigitalDevice
from eheimdigital.types import FilterMode
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import EheimDigitalConfigEntry, EheimDigitalUpdateCoordinator
from .entity import EheimDigitalEntity
PARALLEL_UPDATES = 0
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
@dataclass(frozen=True, kw_only=True)
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
"""Class describing EHEIM Digital select entities."""
value_fn: Callable[[_DeviceT_co], str | None]
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
CLASSICVARIO_DESCRIPTIONS: tuple[
EheimDigitalSelectDescription[EheimDigitalClassicVario], ...
] = (
EheimDigitalSelectDescription[EheimDigitalClassicVario](
key="filter_mode",
translation_key="filter_mode",
value_fn=(
lambda device: device.filter_mode.name.lower()
if device.filter_mode is not None
else None
),
set_value_fn=(
lambda device, value: device.set_filter_mode(FilterMode[value.upper()])
),
options=[name.lower() for name in FilterMode.__members__],
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: EheimDigitalConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the callbacks for the coordinator so select entities can be added as devices are found."""
coordinator = entry.runtime_data
def async_setup_device_entities(
device_address: dict[str, EheimDigitalDevice],
) -> None:
"""Set up the number entities for one or multiple devices."""
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
for device in device_address.values():
if isinstance(device, EheimDigitalClassicVario):
entities.extend(
EheimDigitalSelect[EheimDigitalClassicVario](
coordinator, device, description
)
for description in CLASSICVARIO_DESCRIPTIONS
)
async_add_entities(entities)
coordinator.add_platform_callback(async_setup_device_entities)
async_setup_device_entities(coordinator.hub.devices)
class EheimDigitalSelect(
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
):
"""Represent an EHEIM Digital select entity."""
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
def __init__(
self,
coordinator: EheimDigitalUpdateCoordinator,
device: _DeviceT_co,
description: EheimDigitalSelectDescription[_DeviceT_co],
) -> None:
"""Initialize an EHEIM Digital select entity."""
super().__init__(coordinator, device)
self.entity_description = description
self._attr_unique_id = f"{self._device_address}_{description.key}"
@override
async def async_select_option(self, option: str) -> None:
return await self.entity_description.set_value_fn(self._device, option)
@override
def _async_update_attrs(self) -> None:
self._attr_current_option = self.entity_description.value_fn(self._device)
@@ -62,6 +62,19 @@
},
"night_temperature_offset": {
"name": "Night temperature offset"
},
"system_led": {
"name": "System LED brightness"
}
},
"select": {
"filter_mode": {
"name": "Filter mode",
"state": {
"manual": "Manual",
"pulse": "Pulse",
"bio": "Bio"
}
}
},
"sensor": {
-36
View File
@@ -20,10 +20,8 @@ from homeassistant.components.climate import (
from homeassistant.const import PRECISION_WHOLE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from . import ElkM1ConfigEntry
from .const import DOMAIN
from .entity import ElkEntity, create_elk_entities
SUPPORT_HVAC = [
@@ -78,7 +76,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
_attr_precision = PRECISION_WHOLE
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.AUX_HEAT
| ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
@@ -128,11 +125,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
"""Return the current humidity."""
return self._element.humidity
@property
def is_aux_heat(self) -> bool:
"""Return if aux heater is on."""
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
@property
def fan_mode(self) -> str | None:
"""Return the fan setting."""
@@ -151,34 +143,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
thermostat_mode, fan_mode = HASS_TO_ELK_HVAC_MODES[hvac_mode]
self._elk_set(thermostat_mode, fan_mode)
async def async_turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.EMERGENCY_HEAT, None)
async def async_turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
async_create_issue(
self.hass,
DOMAIN,
"migrate_aux_heat",
breaks_in_ha_version="2025.4.0",
is_fixable=True,
is_persistent=True,
translation_key="migrate_aux_heat",
severity=IssueSeverity.WARNING,
)
self._elk_set(ThermostatMode.HEAT, None)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
thermostat_mode, elk_fan_mode = HASS_TO_ELK_FAN_MODES[fan_mode]
@@ -189,18 +189,5 @@
"name": "Sensor zone trigger",
"description": "Triggers zone."
}
},
"issues": {
"migrate_aux_heat": {
"title": "Migration of Elk-M1 set_aux_heat action",
"fix_flow": {
"step": {
"confirm": {
"description": "The Elk-M1 `set_aux_heat` action has been migrated. A new emergency heat switch entity is available for each thermostat.\n\nUpdate any automations to use the new emergency heat switch entity. When this is done, select **Submit** to fix this issue.",
"title": "[%key:component::elkm1::issues::migrate_aux_heat::title%]"
}
}
}
}
}
}
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.13.7"]
"requirements": ["sense-energy==0.13.8"]
}
@@ -11,7 +11,6 @@ from pyephember2.pyephember2 import (
ZoneMode,
zone_current_temperature,
zone_is_active,
zone_is_boost_active,
zone_is_hotwater,
zone_mode,
zone_name,
@@ -102,7 +101,6 @@ class EphEmberThermostat(ClimateEntity):
self._attr_name = self._zone_name
if self._hot_water:
self._attr_supported_features = ClimateEntityFeature.AUX_HEAT
self._attr_target_temperature_step = None
else:
self._attr_target_temperature_step = 0.5
@@ -144,22 +142,6 @@ class EphEmberThermostat(ClimateEntity):
else:
_LOGGER.error("Invalid operation mode provided %s", hvac_mode)
@property
def is_aux_heat(self) -> bool:
"""Return true if aux heater."""
return zone_is_boost_active(self._zone)
def turn_aux_heat_on(self) -> None:
"""Turn auxiliary heater on."""
self._ember.activate_boost_by_name(
self._zone_name, zone_target_temperature(self._zone)
)
def turn_aux_heat_off(self) -> None:
"""Turn auxiliary heater off."""
self._ember.deactivate_boost_by_name(self._zone_name)
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
+5 -1
View File
@@ -239,7 +239,6 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
self._states = cast(dict[int, _StateT], entry_data.state[state_type])
assert entry_data.device_info is not None
device_info = entry_data.device_info
self._device_info = device_info
self._on_entry_data_changed()
self._key = entity_info.key
self._state_type = state_type
@@ -327,6 +326,11 @@ class EsphomeEntity(EsphomeBaseEntity, Generic[_InfoT, _StateT]):
@callback
def _on_entry_data_changed(self) -> None:
entry_data = self._entry_data
# Update the device info since it can change
# when the device is reconnected
if TYPE_CHECKING:
assert entry_data.device_info is not None
self._device_info = entry_data.device_info
self._api_version = entry_data.api_version
self._client = entry_data.client
if self._device_info.has_deep_sleep:
+3 -3
View File
@@ -63,7 +63,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if self._supports_speed_levels:
data["speed_level"] = math.ceil(
percentage_to_ranged_value(
(1, self._static_info.supported_speed_levels), percentage
(1, self._static_info.supported_speed_count), percentage
)
)
else:
@@ -121,7 +121,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
)
return ranged_value_to_percentage(
(1, self._static_info.supported_speed_levels), self._state.speed_level
(1, self._static_info.supported_speed_count), self._state.speed_level
)
@property
@@ -164,7 +164,7 @@ class EsphomeFan(EsphomeEntity[FanInfo, FanState], FanEntity):
if not supports_speed_levels:
self._attr_speed_count = len(ORDERED_NAMED_FAN_SPEEDS)
else:
self._attr_speed_count = static_info.supported_speed_levels
self._attr_speed_count = static_info.supported_speed_count
async_setup_entry = partial(
+7 -6
View File
@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Any, cast
from aioesphomeapi import (
APIVersion,
ColorMode as ESPHomeColorMode,
EntityInfo,
LightColorCapability,
LightInfo,
@@ -106,15 +107,15 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
@lru_cache
def _color_mode_to_ha(mode: int) -> str:
def _color_mode_to_ha(mode: ESPHomeColorMode) -> ColorMode:
"""Convert an esphome color mode to a HA color mode constant.
Choose the color mode that best matches the feature-set.
"""
candidates = []
candidates: list[tuple[ColorMode, LightColorCapability]] = []
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
for caps in cap_lists:
if caps == mode:
if caps.value == mode:
# exact match
return ha_mode
if (mode & caps) == caps:
@@ -131,8 +132,8 @@ def _color_mode_to_ha(mode: int) -> str:
@lru_cache
def _filter_color_modes(
supported: list[int], features: LightColorCapability
) -> tuple[int, ...]:
supported: list[ESPHomeColorMode], features: LightColorCapability
) -> tuple[ESPHomeColorMode, ...]:
"""Filter the given supported color modes.
Excluding all values that don't have the requested features.
@@ -156,7 +157,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
"""A light implementation for ESPHome."""
_native_supported_color_modes: tuple[int, ...]
_native_supported_color_modes: tuple[ESPHomeColorMode, ...]
_supports_color_mode = False
@property
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==30.2.0",
"aioesphomeapi==31.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1"
],
+2 -2
View File
@@ -88,9 +88,9 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
return
if (
state_class == EsphomeSensorStateClass.MEASUREMENT
and static_info.last_reset_type == LastResetType.AUTO
and static_info.legacy_last_reset_type == LastResetType.AUTO
):
# Legacy, last_reset_type auto was the equivalent to the
# Legacy, legacy_last_reset_type auto was the equivalent to the
# TOTAL_INCREASING state class
self._attr_state_class = SensorStateClass.TOTAL_INCREASING
else:
@@ -35,7 +35,7 @@ async def validate_host(
hass: HomeAssistant, host: str
) -> tuple[str, FroniusConfigEntryData]:
"""Validate the user input allows us to connect."""
fronius = Fronius(async_get_clientsession(hass), host)
fronius = Fronius(async_get_clientsession(hass, verify_ssl=False), host)
try:
datalogger_info: dict[str, Any]
+2 -2
View File
@@ -4,13 +4,13 @@
"current_dc": {
"default": "mdi:current-dc"
},
"current_dc_2": {
"current_dc_mppt_no": {
"default": "mdi:current-dc"
},
"voltage_dc": {
"default": "mdi:current-dc"
},
"voltage_dc_2": {
"voltage_dc_mppt_no": {
"default": "mdi:current-dc"
},
"co2_factor": {
+41 -1
View File
@@ -168,6 +168,26 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="current_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="current_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
translation_key="current_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
FroniusSensorEntityDescription(
key="power_ac",
@@ -197,6 +217,26 @@ INVERTER_ENTITY_DESCRIPTIONS: list[FroniusSensorEntityDescription] = [
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "2"},
),
FroniusSensorEntityDescription(
key="voltage_dc_3",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "3"},
),
FroniusSensorEntityDescription(
key="voltage_dc_4",
default_value=0,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
translation_key="voltage_dc_mppt_no",
translation_placeholders={"mppt_no": "4"},
),
# device status entities
FroniusSensorEntityDescription(
@@ -727,7 +767,7 @@ class _FroniusSensorEntity(CoordinatorEntity["FroniusCoordinatorBase"], SensorEn
self.response_key = description.response_key or description.key
self.solar_net_id = solar_net_id
self._attr_native_value = self._get_entity_value()
self._attr_translation_key = description.key
self._attr_translation_key = description.translation_key or description.key
def _device_data(self) -> dict[str, Any]:
"""Extract information for SolarNet device from coordinator data."""
@@ -52,8 +52,8 @@
"current_dc": {
"name": "DC current"
},
"current_dc_2": {
"name": "DC current 2"
"current_dc_mppt_no": {
"name": "DC current {mppt_no}"
},
"power_ac": {
"name": "AC power"
@@ -64,8 +64,8 @@
"voltage_dc": {
"name": "DC voltage"
},
"voltage_dc_2": {
"name": "DC voltage 2"
"voltage_dc_mppt_no": {
"name": "DC voltage {mppt_no}"
},
"inverter_state": {
"name": "Inverter state"
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250509.0"]
"requirements": ["home-assistant-frontend==20250516.0"]
}
+11 -11
View File
@@ -20,9 +20,12 @@ from homeassistant.helpers import config_entry_flow, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType
from homeassistant.util import slugify
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN
type GeofencyConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
CONF_MOBILE_BEACONS = "mobile_beacons"
@@ -75,16 +78,13 @@ WEBHOOK_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
_DATA_GEOFENCY: HassKey[list[str]] = HassKey(DOMAIN)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Geofency component."""
config = hass_config.get(DOMAIN, {})
mobile_beacons = config.get(CONF_MOBILE_BEACONS, [])
hass.data[DOMAIN] = {
"beacons": [slugify(beacon) for beacon in mobile_beacons],
"devices": set(),
"unsub_device_tracker": {},
}
mobile_beacons = hass_config.get(DOMAIN, {}).get(CONF_MOBILE_BEACONS, [])
hass.data[_DATA_GEOFENCY] = [slugify(beacon) for beacon in mobile_beacons]
return True
@@ -99,7 +99,7 @@ async def handle_webhook(
text=error.error_message, status=HTTPStatus.UNPROCESSABLE_ENTITY
)
if _is_mobile_beacon(data, hass.data[DOMAIN]["beacons"]):
if _is_mobile_beacon(data, hass.data[_DATA_GEOFENCY]):
return _set_location(hass, data, None)
if data["entry"] == LOCATION_ENTRY:
location_name = data["name"]
@@ -140,8 +140,9 @@ def _set_location(hass, data, location_name):
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
"""Configure based on config entry."""
entry.runtime_data = set()
webhook.async_register(
hass, DOMAIN, "Geofency", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -150,10 +151,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: GeofencyConfigEntry) -> bool:
"""Unload a config entry."""
webhook.async_unregister(hass, entry.data[CONF_WEBHOOK_ID])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -1,7 +1,6 @@
"""Support for the Geofency device tracker platform."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
@@ -10,12 +9,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import DOMAIN, TRACKER_UPDATE
from . import TRACKER_UPDATE, GeofencyConfigEntry
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: GeofencyConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Geofency config entry."""
@@ -23,14 +23,16 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, location_name, attributes):
"""Fire HA event to set location."""
if device in hass.data[DOMAIN]["devices"]:
if device in config_entry.runtime_data:
return
hass.data[DOMAIN]["devices"].add(device)
config_entry.runtime_data.add(device)
async_add_entities([GeofencyEntity(device, gps, location_name, attributes)])
async_add_entities(
[GeofencyEntity(config_entry, device, gps, location_name, attributes)]
)
hass.data[DOMAIN]["unsub_device_tracker"][config_entry.entry_id] = (
config_entry.async_on_unload(
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
@@ -45,8 +47,8 @@ async def async_setup_entry(
}
if dev_ids:
hass.data[DOMAIN]["devices"].update(dev_ids)
async_add_entities(GeofencyEntity(dev_id) for dev_id in dev_ids)
config_entry.runtime_data.update(dev_ids)
async_add_entities(GeofencyEntity(config_entry, dev_id) for dev_id in dev_ids)
class GeofencyEntity(TrackerEntity, RestoreEntity):
@@ -55,8 +57,9 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
_attr_has_entity_name = True
_attr_name = None
def __init__(self, device, gps=None, location_name=None, attributes=None):
def __init__(self, entry, device, gps=None, location_name=None, attributes=None):
"""Set up Geofency entity."""
self._entry = entry
self._attr_extra_state_attributes = attributes or {}
self._name = device
self._attr_location_name = location_name
@@ -93,7 +96,7 @@ class GeofencyEntity(TrackerEntity, RestoreEntity):
"""Clean up after entity before removal."""
await super().async_will_remove_from_hass()
self._unsub_dispatcher()
self.hass.data[DOMAIN]["devices"].remove(self.unique_id)
self._entry.runtime_data.remove(self.unique_id)
@callback
def _async_receive_data(self, device, gps, location_name, attributes):
@@ -8,6 +8,6 @@
"integration_type": "system",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["go2rtc-client==0.1.2"],
"requirements": ["go2rtc-client==0.1.3b0"],
"single_config_entry": true
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.2"]
"requirements": ["gcal-sync==7.0.1", "oauth2client==4.1.3", "ical==9.2.4"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/google_travel_time",
"iot_class": "cloud_polling",
"loggers": ["google", "homeassistant.helpers.location"],
"requirements": ["google-maps-routing==0.6.14"]
"requirements": ["google-maps-routing==0.6.15"]
}
@@ -24,6 +24,8 @@ from .const import (
DOMAIN,
)
type GPSLoggerConfigEntry = ConfigEntry[set[str]]
PLATFORMS = [Platform.DEVICE_TRACKER]
TRACKER_UPDATE = f"{DOMAIN}_tracker_update"
@@ -88,9 +90,9 @@ async def handle_webhook(
return web.Response(text=f"Setting location for {device}")
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GPSLoggerConfigEntry) -> bool:
"""Configure based on config entry."""
hass.data.setdefault(DOMAIN, {"devices": set(), "unsub_device_tracker": {}})
entry.runtime_data = set()
webhook.async_register(
hass, DOMAIN, "GPSLogger", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
@@ -103,7 +105,6 @@ 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])
hass.data[DOMAIN]["unsub_device_tracker"].pop(entry.entry_id)()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@@ -1,7 +1,6 @@
"""Support for the GPSLogger device tracking."""
from homeassistant.components.device_tracker import TrackerEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_GPS_ACCURACY,
@@ -15,19 +14,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import DOMAIN, TRACKER_UPDATE
from . import TRACKER_UPDATE, GPSLoggerConfigEntry
from .const import (
ATTR_ACTIVITY,
ATTR_ALTITUDE,
ATTR_DIRECTION,
ATTR_PROVIDER,
ATTR_SPEED,
DOMAIN,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: GPSLoggerConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Configure a dispatcher connection based on a config entry."""
@@ -35,16 +35,14 @@ async def async_setup_entry(
@callback
def _receive_data(device, gps, battery, accuracy, attrs):
"""Receive set location."""
if device in hass.data[DOMAIN]["devices"]:
if device in entry.runtime_data:
return
hass.data[DOMAIN]["devices"].add(device)
entry.runtime_data.add(device)
async_add_entities([GPSLoggerEntity(device, gps, battery, accuracy, attrs)])
hass.data[DOMAIN]["unsub_device_tracker"][entry.entry_id] = (
async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data)
)
entry.async_on_unload(async_dispatcher_connect(hass, TRACKER_UPDATE, _receive_data))
# Restore previously loaded devices
dev_reg = dr.async_get(hass)
@@ -58,7 +56,7 @@ async def async_setup_entry(
entities = []
for dev_id in dev_ids:
hass.data[DOMAIN]["devices"].add(dev_id)
entry.runtime_data.add(dev_id)
entity = GPSLoggerEntity(dev_id, None, None, None, None)
entities.append(entity)
+10 -23
View File
@@ -1,33 +1,29 @@
"""The Gree Climate integration."""
from __future__ import annotations
from datetime import timedelta
import logging
from homeassistant.components.network import async_get_ipv4_broadcast_addresses
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_time_interval
from .const import (
COORDINATORS,
DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL,
DISPATCHERS,
DOMAIN,
)
from .coordinator import DiscoveryService
from .const import DISCOVERY_SCAN_INTERVAL
from .coordinator import DiscoveryService, GreeConfigEntry, GreeRuntimeData
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.CLIMATE, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
"""Set up Gree Climate from a config entry."""
hass.data.setdefault(DOMAIN, {})
gree_discovery = DiscoveryService(hass, entry)
hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery
entry.runtime_data = GreeRuntimeData(
discovery_service=gree_discovery, coordinators=[]
)
async def _async_scan_update(_=None):
bcast_addr = list(await async_get_ipv4_broadcast_addresses(hass))
@@ -47,15 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: GreeConfigEntry) -> bool:
"""Unload a config entry."""
if hass.data.get(DATA_DISCOVERY_SERVICE) is not None:
hass.data.pop(DATA_DISCOVERY_SERVICE)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(COORDINATORS, None)
hass.data[DOMAIN].pop(DISPATCHERS, None)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
+4 -7
View File
@@ -36,21 +36,18 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW,
TARGET_TEMPERATURE_STEP,
)
from .coordinator import DeviceDataUpdateCoordinator
from .coordinator import DeviceDataUpdateCoordinator, GreeConfigEntry
from .entity import GreeEntity
_LOGGER = logging.getLogger(__name__)
@@ -87,17 +84,17 @@ SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: GreeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator):
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
"""Register the device."""
async_add_entities([GreeClimateEntity(coordinator)])
for coordinator in hass.data[DOMAIN][COORDINATORS]:
for coordinator in entry.runtime_data.coordinators:
init_device(coordinator)
entry.async_on_unload(
-6
View File
@@ -1,16 +1,10 @@
"""Constants for the Gree Climate integration."""
COORDINATORS = "coordinators"
DATA_DISCOVERY_SERVICE = "gree_discovery"
DISCOVERY_SCAN_INTERVAL = 300
DISCOVERY_TIMEOUT = 8
DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered"
DISPATCHERS = "dispatchers"
DOMAIN = "gree"
COORDINATOR = "coordinator"
FAN_MEDIUM_LOW = "medium low"
FAN_MEDIUM_HIGH = "medium high"
+16 -8
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import copy
from dataclasses import dataclass
from datetime import datetime, timedelta
import logging
from typing import Any
@@ -20,7 +21,6 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from homeassistant.util.dt import utcnow
from .const import (
COORDINATORS,
DISCOVERY_TIMEOUT,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
@@ -31,14 +31,24 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
type GreeConfigEntry = ConfigEntry[GreeRuntimeData]
@dataclass
class GreeRuntimeData:
"""RUntime data for Gree Climate integration."""
discovery_service: DiscoveryService
coordinators: list[DeviceDataUpdateCoordinator]
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages polling for state changes from the device."""
config_entry: ConfigEntry
config_entry: GreeConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Device
self, hass: HomeAssistant, config_entry: GreeConfigEntry, device: Device
) -> None:
"""Initialize the data update coordinator."""
super().__init__(
@@ -128,7 +138,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
class DiscoveryService(Listener):
"""Discovery event handler for gree devices."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: GreeConfigEntry) -> None:
"""Initialize discovery service."""
super().__init__()
self.hass = hass
@@ -137,8 +147,6 @@ class DiscoveryService(Listener):
self.discovery = Discovery(DISCOVERY_TIMEOUT)
self.discovery.add_listener(self)
hass.data[DOMAIN].setdefault(COORDINATORS, [])
async def device_found(self, device_info: DeviceInfo) -> None:
"""Handle new device found on the network."""
@@ -157,14 +165,14 @@ class DiscoveryService(Listener):
device.device_info.port,
)
coordo = DeviceDataUpdateCoordinator(self.hass, self.entry, device)
self.hass.data[DOMAIN][COORDINATORS].append(coordo)
self.entry.runtime_data.coordinators.append(coordo)
await coordo.async_refresh()
async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo)
async def device_update(self, device_info: DeviceInfo) -> None:
"""Handle updates in device information, update if ip has changed."""
for coordinator in self.hass.data[DOMAIN][COORDINATORS]:
for coordinator in self.entry.runtime_data.coordinators:
if coordinator.device.device_info.mac == device_info.mac:
coordinator.device.device_info.ip = device_info.ip
await coordinator.async_refresh()
+6 -6
View File
@@ -13,13 +13,13 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DOMAIN
from .entity import GreeEntity
from .const import DISPATCH_DEVICE_DISCOVERED
from .coordinator import GreeConfigEntry
from .entity import DeviceDataUpdateCoordinator, GreeEntity
@dataclass(kw_only=True, frozen=True)
@@ -92,13 +92,13 @@ GREE_SWITCHES: tuple[GreeSwitchEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: GreeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gree HVAC device from a config entry."""
@callback
def init_device(coordinator):
def init_device(coordinator: DeviceDataUpdateCoordinator) -> None:
"""Register the device."""
async_add_entities(
@@ -106,7 +106,7 @@ async def async_setup_entry(
for description in GREE_SWITCHES
)
for coordinator in hass.data[DOMAIN][COORDINATORS]:
for coordinator in entry.runtime_data.coordinators:
init_device(coordinator)
entry.async_on_unload(
@@ -1,5 +1,14 @@
"""Shared constants for the greeneye_monitor integration."""
from __future__ import annotations
from typing import TYPE_CHECKING
from homeassistant.util.hass_dict import HassKey
if TYPE_CHECKING:
from greeneye import Monitors
CONF_CHANNELS = "channels"
CONF_COUNTED_QUANTITY = "counted_quantity"
CONF_COUNTED_QUANTITY_PER_PULSE = "counted_quantity_per_pulse"
@@ -13,8 +22,8 @@ CONF_TEMPERATURE_SENSORS = "temperature_sensors"
CONF_TIME_UNIT = "time_unit"
CONF_VOLTAGE_SENSORS = "voltage"
DATA_GREENEYE_MONITOR = "greeneye_monitor"
DOMAIN = "greeneye_monitor"
DATA_GREENEYE_MONITOR: HassKey[Monitors] = HassKey(DOMAIN)
SENSOR_TYPE_CURRENT = "current_sensor"
SENSOR_TYPE_PULSE_COUNTER = "pulse_counter"
@@ -109,7 +109,7 @@ async def async_setup_platform(
if len(monitor_configs) == 0:
monitors.remove_listener(on_new_monitor)
monitors: greeneye.Monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors = hass.data[DATA_GREENEYE_MONITOR]
monitors.add_listener(on_new_monitor)
for monitor in monitors.monitors.values():
on_new_monitor(monitor)
@@ -52,10 +52,10 @@ type HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
"""Habitica Data Update Coordinator."""
config_entry: ConfigEntry
config_entry: HabiticaConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, habitica: Habitica
self, hass: HomeAssistant, config_entry: HabiticaConfigEntry, habitica: Habitica
) -> None:
"""Initialize the Habitica data coordinator."""
super().__init__(
@@ -234,7 +234,7 @@
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
"consumer_products_coffee_maker_program_beverage_hot_water": "Hot water",
"dishcare_dishwasher_program_pre_rinse": "Pre_rinse",
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
@@ -252,7 +252,7 @@
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
"dishcare_dishwasher_program_magic_daily": "Magic daily",
"dishcare_dishwasher_program_super_60": "Super 60ºC",
"dishcare_dishwasher_program_kurz_60": "Kurz 60ºC",
"dishcare_dishwasher_program_kurz_60": "Speed 60ºC",
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
"dishcare_dishwasher_program_machine_care": "Machine care",
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
@@ -90,16 +90,17 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
minor_version=2,
)
if config_entry.minor_version == 2:
# Add a `firmware_version` key
if config_entry.minor_version <= 3:
# Add a `firmware_version` key if it doesn't exist to handle entries created
# with minor version 1.3 where the firmware version was not set.
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
FIRMWARE_VERSION: None,
FIRMWARE_VERSION: config_entry.data.get(FIRMWARE_VERSION),
},
version=1,
minor_version=3,
minor_version=4,
)
_LOGGER.debug(
@@ -62,7 +62,7 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
"""Handle a config flow for Home Assistant Yellow."""
VERSION = 1
MINOR_VERSION = 3
MINOR_VERSION = 4
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Instantiate config flow."""
@@ -116,6 +116,11 @@ class HomeAssistantYellowConfigFlow(BaseFirmwareConfigFlow, domain=DOMAIN):
if self._probed_firmware_info is not None
else ApplicationType.EZSP
).value,
FIRMWARE_VERSION: (
self._probed_firmware_info.firmware_version
if self._probed_firmware_info is not None
else None
),
},
)
@@ -15,6 +15,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [
Platform.ALARM_CONTROL_PANEL,
Platform.BINARY_SENSOR,
Platform.BUTTON,
Platform.CLIMATE,
@@ -0,0 +1,138 @@
"""The Homee alarm control panel platform."""
from dataclasses import dataclass
from pyHomee.const import AttributeChangedBy, AttributeType
from pyHomee.model import HomeeAttribute
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
AlarmControlPanelEntityDescription,
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, HomeeConfigEntry
from .entity import HomeeEntity
from .helpers import get_name_for_enum
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class HomeeAlarmControlPanelEntityDescription(AlarmControlPanelEntityDescription):
"""A class that describes Homee alarm control panel entities."""
code_arm_required: bool = False
state_list: list[AlarmControlPanelState]
ALARM_DESCRIPTIONS = {
AttributeType.HOMEE_MODE: HomeeAlarmControlPanelEntityDescription(
key="homee_mode",
code_arm_required=False,
state_list=[
AlarmControlPanelState.ARMED_HOME,
AlarmControlPanelState.ARMED_NIGHT,
AlarmControlPanelState.ARMED_AWAY,
AlarmControlPanelState.ARMED_VACATION,
],
)
}
def get_supported_features(
state_list: list[AlarmControlPanelState],
) -> AlarmControlPanelEntityFeature:
"""Return supported features based on the state list."""
supported_features = AlarmControlPanelEntityFeature(0)
if AlarmControlPanelState.ARMED_HOME in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_HOME
if AlarmControlPanelState.ARMED_AWAY in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_AWAY
if AlarmControlPanelState.ARMED_NIGHT in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_NIGHT
if AlarmControlPanelState.ARMED_VACATION in state_list:
supported_features |= AlarmControlPanelEntityFeature.ARM_VACATION
return supported_features
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HomeeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add the Homee platform for the alarm control panel component."""
async_add_entities(
HomeeAlarmPanel(attribute, config_entry, ALARM_DESCRIPTIONS[attribute.type])
for node in config_entry.runtime_data.nodes
for attribute in node.attributes
if attribute.type in ALARM_DESCRIPTIONS and attribute.editable
)
class HomeeAlarmPanel(HomeeEntity, AlarmControlPanelEntity):
"""Representation of a Homee alarm control panel."""
entity_description: HomeeAlarmControlPanelEntityDescription
def __init__(
self,
attribute: HomeeAttribute,
entry: HomeeConfigEntry,
description: HomeeAlarmControlPanelEntityDescription,
) -> None:
"""Initialize a Homee alarm control panel entity."""
super().__init__(attribute, entry)
self.entity_description = description
self._attr_code_arm_required = description.code_arm_required
self._attr_supported_features = get_supported_features(description.state_list)
self._attr_translation_key = description.key
@property
def alarm_state(self) -> AlarmControlPanelState:
"""Return current state."""
return self.entity_description.state_list[int(self._attribute.current_value)]
@property
def changed_by(self) -> str:
"""Return by whom or what the entity was last changed."""
changed_by_name = get_name_for_enum(
AttributeChangedBy, self._attribute.changed_by
)
return f"{changed_by_name} - {self._attribute.changed_by_id}"
async def _async_set_alarm_state(self, state: AlarmControlPanelState) -> None:
"""Set the alarm state."""
if state in self.entity_description.state_list:
await self.async_set_homee_value(
self.entity_description.state_list.index(state)
)
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
# Since disarm is always present in the UI, we raise an error.
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="disarm_not_supported",
)
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_HOME)
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_NIGHT)
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_AWAY)
async def async_alarm_arm_vacation(self, code: str | None = None) -> None:
"""Send arm vacation command."""
await self._async_set_alarm_state(AlarmControlPanelState.ARMED_VACATION)
+14 -8
View File
@@ -27,14 +27,20 @@ class HomeeEntity(Entity):
)
self._entry = entry
node = entry.runtime_data.get_node_by_id(attribute.node_id)
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
# Homee hub itself has node-id -1
if node.id == -1:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
)
else:
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, f"{entry.runtime_data.settings.uid}-{attribute.node_id}")
},
name=node.name,
model=get_name_for_enum(NodeProfile, node.profile),
via_device=(DOMAIN, entry.runtime_data.settings.uid),
)
self._host_connected = entry.runtime_data.connected
@@ -26,6 +26,11 @@
}
},
"entity": {
"alarm_control_panel": {
"homee_mode": {
"name": "Status"
}
},
"binary_sensor": {
"blackout_alarm": {
"name": "Blackout"
@@ -370,6 +375,9 @@
"connection_closed": {
"message": "Could not connect to homee while setting attribute."
},
"disarm_not_supported": {
"message": "Disarm is not supported by homee."
},
"invalid_preset_mode": {
"message": "Invalid preset mode: {preset_mode}. Turning on is only supported with preset mode 'Manual'."
}
@@ -3,7 +3,6 @@
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
@@ -21,7 +20,7 @@ from .const import (
HMIPC_HAPID,
HMIPC_NAME,
)
from .hap import HomematicipHAP
from .hap import HomematicIPConfigEntry, HomematicipHAP
from .services import async_setup_services, async_unload_services
CONFIG_SCHEMA = vol.Schema(
@@ -45,8 +44,6 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the HomematicIP Cloud component."""
hass.data[DOMAIN] = {}
accesspoints = config.get(DOMAIN, [])
for conf in accesspoints:
@@ -69,7 +66,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: HomematicIPConfigEntry) -> bool:
"""Set up an access point from a config entry."""
# 0.104 introduced config entry unique id, this makes upgrading possible
@@ -81,8 +78,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
hap = HomematicipHAP(hass, entry)
hass.data[DOMAIN][entry.unique_id] = hap
entry.runtime_data = hap
if not await hap.async_setup():
return False
@@ -110,9 +107,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: HomematicIPConfigEntry
) -> bool:
"""Unload a config entry."""
hap = hass.data[DOMAIN].pop(entry.unique_id)
hap = entry.runtime_data
assert hap.reset_connection_listener is not None
hap.reset_connection_listener()
await async_unload_services(hass)
@@ -122,7 +122,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@callback
def _async_remove_obsolete_entities(
hass: HomeAssistant, entry: ConfigEntry, hap: HomematicipHAP
hass: HomeAssistant, entry: HomematicIPConfigEntry, hap: HomematicipHAP
):
"""Remove obsolete entities from entity registry."""
@@ -11,13 +11,12 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntityFeature,
AlarmControlPanelState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .hap import AsyncHome, HomematicipHAP
from .hap import AsyncHome, HomematicIPConfigEntry, HomematicipHAP
_LOGGER = logging.getLogger(__name__)
@@ -26,11 +25,11 @@ CONST_ALARM_CONTROL_PANEL_NAME = "HmIP Alarm Control Panel"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomematicIPConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP alrm control panel from a config entry."""
hap = hass.data[DOMAIN][config_entry.unique_id]
hap = config_entry.runtime_data
async_add_entities([HomematicipAlarmControlPanelEntity(hap)])
@@ -34,14 +34,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicipHAP
from .hap import HomematicIPConfigEntry, HomematicipHAP
ATTR_ACCELERATION_SENSOR_MODE = "acceleration_sensor_mode"
ATTR_ACCELERATION_SENSOR_NEUTRAL_POSITION = "acceleration_sensor_neutral_position"
@@ -75,11 +74,11 @@ SAM_DEVICE_ATTRIBUTES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomematicIPConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP Cloud binary sensor from a config entry."""
hap = hass.data[DOMAIN][config_entry.unique_id]
hap = config_entry.runtime_data
entities: list[HomematicipGenericEntity] = [HomematicipCloudConnectionSensor(hap)]
for device in hap.home.devices:
if isinstance(device, AccelerationSensor):
@@ -5,22 +5,20 @@ from __future__ import annotations
from homematicip.device import WallMountedGarageDoorController
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicipHAP
from .hap import HomematicIPConfigEntry, HomematicipHAP
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomematicIPConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP button from a config entry."""
hap = hass.data[DOMAIN][config_entry.unique_id]
hap = config_entry.runtime_data
async_add_entities(
HomematicipGarageDoorControllerButton(hap, device)
@@ -24,7 +24,6 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@@ -32,7 +31,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import HomematicipGenericEntity
from .hap import HomematicipHAP
from .hap import HomematicIPConfigEntry, HomematicipHAP
HEATING_PROFILES = {"PROFILE_1": 0, "PROFILE_2": 1, "PROFILE_3": 2}
COOLING_PROFILES = {"PROFILE_4": 3, "PROFILE_5": 4, "PROFILE_6": 5}
@@ -55,11 +54,11 @@ HMIP_ECO_CM = "ECO"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HomematicIPConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the HomematicIP climate from a config entry."""
hap = hass.data[DOMAIN][config_entry.unique_id]
hap = config_entry.runtime_data
async_add_entities(
HomematicipHeatingGroup(hap, device)

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