Compare commits

..

127 Commits

Author SHA1 Message Date
epenet ae3c892ac3 Update homeassistant/components/renault/quality_scale.yaml 2025-04-28 08:52:39 +02:00
epenet 1b2c1f220f Mark dynamic-devices as exempt in Renault IQS 2025-04-28 07:43:57 +02:00
J. Nick Koston dd9dad80be Bump habluetooth to 3.42.0 and bleak-esphome to 2.14.0 (#143787) 2025-04-27 19:36:58 -05:00
Åke Strandberg 9992ade051 Bump pymiele to 0.4.0 (#143789) 2025-04-27 23:31:10 +01:00
J. Nick Koston 36da4a9b72 Bump bluetooth-data-tools to 1.28.0 (#143782)
changelog: https://github.com/Bluetooth-Devices/bluetooth-data-tools/compare/v1.27.0...v1.28.0

related issue https://github.com/home-assistant/core/issues/143769#issuecomment-2833594159
2025-04-27 16:50:42 -05:00
Mick Vleeshouwer 3fc34244ac Fix hvac_mode property to handle missing CORE_ON_OFF state in Atlantic Electrical Heater in Overkiz (#143330) 2025-04-27 20:42:51 +02:00
tronikos 753c07e911 Bump opower to 0.12.0 (#143748) 2025-04-27 20:40:10 +02:00
Joris Drenth d0850e2931 Bump Wallbox version to 0.9.0 (#143775)
Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-04-27 20:36:20 +02:00
Manu c704df004a Add diagnostics platform to ntfy platform (#143774) 2025-04-27 19:58:15 +02:00
Yuxin Wang d95c9c496e Make exception messages translatable for APCUPSD (#143747)
Add translation domain and key for UpdateFailed in coordinator
2025-04-27 16:35:55 +02:00
Ville Skyttä d28f4ed618 Set device class for huawei_lte connectivity binary sensors (#143764) 2025-04-27 16:34:11 +02:00
Jan Bouwhuis 7a0580eff5 Import media player constants at integration level for alexa smart home (#143767) 2025-04-27 15:36:42 +02:00
Sanjay Govind f94af84f2a Update deprecated const usage in alexa integration (#143741) 2025-04-27 14:33:16 +02:00
Allen Porter 31fb199670 Bump voluptuous-openapi to 0.0.7 (#143742) 2025-04-27 12:10:26 +02:00
Brett Adams a1ca0a1cb2 Dont add location entities without location scope in Teslemetry (#143497)
* Dont add location entities without location scope

* Fix tests

* simplify logic

* Add test
2025-04-27 11:25:58 +02:00
Allen Porter 2326c23133 Increase Gemini max tokens to avoid failures observed in evaluations (#143728)
* Increase Gemini max tokens to avoid failures observed in evaluations

* Update snapshots
2025-04-26 15:30:47 -07:00
J. Nick Koston d4c1d1bdb9 Split up SSDP integration into modules (#143732)
* Split up SSDP integration into modules

* Split up SSDP integration into modules

* migrate tests
2025-04-26 18:09:51 -04:00
Allen Porter 8d258871ff Record Anthropic token statistics in conversation trace (#143727)
* Record anthopic token statistics in conversation trace

* Add test coverage for output token parsing
2025-04-26 18:04:12 -04:00
Thomas55555 49299a6bf0 Bump aioautomower to 2025.4.4 (#143533)
* Bump aioautomower to 2025.4.1

* Update split_tests.py

* revert b3222b9be994d39e9e5b28d8e06abeb36bbda6ca

Co-authored-by: Shay Levy <levyshay1@gmail.com>

* aioautomower==2025.4.2

* fix

* aioautomower==2025.4.30b0

* revert

* some try

* aioautomower==2025.4.0

* aioautomower==2025.4.3b0

* aioautomower==2025.4.4

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-04-27 00:07:14 +03:00
Shay Levy 868b8ad318 Move Switcher handle_coordinator_update to base entity (#143738) 2025-04-27 00:01:44 +03:00
Simone Chemelli 40752dcfb6 Translate missing exceptions in SamsungTV (#143628)
* Translate missing exceptions in SamsungTV

* apply review comment
2025-04-26 22:43:07 +02:00
Stefan Agner 18f51abfe6 Remove unnecessary Supervisor info call (#143700) 2025-04-26 15:27:31 -05:00
Maikel Punie 3e2c54dcbd Bump velbusaio to 2025.4.2 (#143675) 2025-04-26 15:22:10 -05:00
Manu a0cd14b4e8 Add reauth flow to ntfy integration (#143729) 2025-04-26 22:05:13 +02:00
Norbert Rittel 35c6fdbce8 Use common state for "Fault" in shelly (#143730) 2025-04-26 21:08:39 +03:00
sebfortier2288 202addc39d Remove sebfortier2288 from Soma code owners (#143715)
* chore(soma): remove from codeowner

* chore(soma): remove from sebfortier2288 codeowners
2025-04-26 19:56:56 +03:00
Shay Levy d8cb7c475b Update Switcher temperature sensor device class and state class (#143722)
* Update Switcher temperature sensor device class and state class

* Remove  temperature translation key

* Remove icon
2025-04-26 16:22:44 +02:00
Shay Levy 03bacd747e Use device_registry fixture in Switcher test_remove_device (#143723) 2025-04-26 17:05:51 +03:00
Manu 97b6a68cda Improve device handling for disconnected IronOS devices (#143446)
* Improve device handling for disconnected IronOS devices

* requested changes

* ble_device
2025-04-26 13:34:44 +02:00
Shay Levy eee18035cf Use value_fn in Switcher sensor platform (#143711) 2025-04-26 13:34:13 +02:00
Maciej Bieniek f1b3b0c155 Refactor tests for Shelly config flow (#143517)
* Add mock_setup_entry

* Add mock_setup

* Improve test_form_gen1_custom_port

* Improve test_form_errors_get_info

* Improve test_form_errors_test_connection

* Improve test_reconfigure_with_exception

* Improve test_form_auth_errors_test_connection_gen1

* Improve test_form_auth_errors_test_connection_gen2

* Cleaning

* Upate quality scale

* Always use result variable

* Remove unnecessary async_block_till_done
2025-04-26 13:00:45 +03:00
Åke Strandberg f5d3495c62 Add properties to miele entity class (#143622)
* Add properties to Entity class

* Remove setter and most platform constructors
2025-04-26 09:55:11 +02:00
Martin Hjelmare e14a356c24 Allow Z-Wave controller migration on USB discovery (#143677)
Allow migration on USB discovery
2025-04-26 07:52:32 +02:00
J. Nick Koston 4e7d396e5b Add WebSocket API to zeroconf to observe discovery (#143540)
* Add WebSocket API to zeroconf to observe discovery

* Add WebSocket API to zeroconf to observe discovery

* increase timeout

* cover

* cover

* cover

* cover

* cover

* cover

* fix lasting side effects

* cleanup merge

* format
2025-04-25 21:18:09 -04:00
J. Nick Koston 34d17ca458 Move state length validation to StateMachine APIs (#143681)
* Move state length validation to StateMachine async_set method

We call validate_state to make sure we do not allow any states
into the state machine that have a length>255 so we do not break
the recorder. Since async_set_internal already requires callers
to pre-validate the state, we can move the check to async_set
instead of at State object creation time to avoid needing to
check it twice in the hot path (entity write state)

* move check in async_set_internal so it only happens on state change

* no need to check if same_state
2025-04-25 21:15:15 -04:00
J. Nick Koston 03950f270a Remove lower call in async_reserve (#143682)
async_reserve is only called from the the entity_platform helper
which already ensures the entity_id is validated and in lower
case.
https://github.com/home-assistant/core/blob/a783b6a0abda02b26e193356c4f3db8b86e13b86/homeassistant/helpers/entity_platform.py#L936
2025-04-25 21:12:55 -04:00
Denis Shulyaka 7074331461 Preserve reasoning during tool calls for openai_conversation (#143699)
Preserve reasoning after tool calls for openai_conversation
2025-04-25 21:12:23 -04:00
Maksim Doroshko 4c9cd70f65 Set unique id in ephember (#143180)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-25 23:06:16 +01:00
Ville Skyttä 7a105de969 Add missing huawei_lte sensor translations (#143694) 2025-04-25 22:54:56 +01:00
Åke Strandberg eec9a28fe8 Add zeroconf discovery to miele (#143259)
* Add zeroconf discovery

* Strip unnecessary code

* Remove one line more

* Remove one more

* Add test for zeroconf flow

* Finish zeroconf flow
2025-04-25 23:18:20 +02:00
Arie Catsman 963f1b1907 bump pyenphase to 1.26.0 (#143686) 2025-04-25 08:50:37 -10:00
dependabot[bot] dcac9b5f20 Bump actions/download-artifact from 4.2.1 to 4.3.0 (#143650) 2025-04-25 20:40:18 +02:00
Joost Lekkerkerker 765a95c273 Set entities to config category in SmartThings (#143669) 2025-04-25 20:21:35 +02:00
Tomáš Bedřich 6a115d0133 Add S3 integration (#139325)
* Add S3 integration

* Improve translations and error handling

* Test S3 integration

* Update QoS

* Add missing data_description strings

* Fix missing async_initialize_backup in tests

* PR changes

* Remove unique ID, rely on abort_entries_match

* Raise only BackupAgentError (#139754), introduce decorator for error handling

* Switch to metadata-file based solution

* PR changes

* Revert strict typing

* Bump dependency

* Silence mypy

* Pass docs URLs as description_placeholders

* PR changes

* Rename _api to api

* PR Changes

* PR Changes 2

* Remove api abstraction

* Handle S3 multipart upload size limitations

* PR changes
2025-04-25 20:16:44 +02:00
Åke Strandberg a057effad5 Add miele binary_sensor platform (#142903)
* Add binary_sensor platform

* Address review comments

* Adjust icons and names.

* Change Info to Notification active

* Trigger CI

* Trig CI

* Adjust tests

* Update strings.json

* Update strings.json
2025-04-25 19:32:08 +02:00
Dan 94b0800989 Fix surepetcare sensor error (#143286)
* fix: changed boolean to map to 'online' attribute.

* fix: added catch in case of future changes to prevent complete sensor failure.

* fix: surepetcare - added additional catches in case rssi values aren't included in online status.

* fix: remove hub_rssi when not defined.

* fix: proper code spacing

* fix: use .get for clarity instead of try.

* fix: now written in Python.

* fix: renamed variables for clarity.

* Update homeassistant/components/surepetcare/binary_sensor.py

* fix: update surepetcare test __init__.py mock_feeder with online status.

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-25 19:29:29 +02:00
Åke Strandberg a783b6a0ab Add climate platform to miele integration (#143333)
* Add climate platform

* Merge

* Address review and improve test

* Address review comments

* Streamline entity naming

* Update tests/components/miele/test_climate.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-25 19:18:39 +02:00
Åke Strandberg 5302964eb6 Add button platform to miele (#143508)
* WIP Button platform

* Add button platform

* Disable by default, Address review , update tests

* Follow review comments
2025-04-25 19:10:32 +02:00
Norbert Rittel 261dbd16a6 Add common state "Fault" (#143390) 2025-04-25 18:47:19 +02:00
Guido Schmitz 672dbc03c6 Use coordinator data for devolo Home Network PLC data rate sensor (#143606) 2025-04-25 18:45:16 +02:00
Åke Strandberg ed0bdf9e5f Add switch platform to miele integration (#142925)
* Add switch platform

* Add a type hint

* Update after review
2025-04-25 18:40:52 +02:00
Simone Chemelli 735e2e4192 Add missing exception translations to Comelit (#142861)
* Add missing exception translations to Comelit

* update quality scale

* remove unwanted placeholder
2025-04-25 18:34:29 +02:00
Martin Hjelmare 0aabb11220 Improve Z-Wave migration flow (#143673) 2025-04-25 18:33:19 +02:00
Jozef Kruszynski 09ad14bc28 Update Music Assistant browse media types (#143249)
* Update Music Assistant browse media types

* changes based on review comments
2025-04-25 18:32:48 +02:00
J. Nick Koston d61e39743b Reduce ref counting in _async_write_ha_state (#143634)
* Reduce ref counting in _async_write_ha_state

It no longer makes sense to keep a temp reference
to entity_id and hass since the function was
refactored and there are very few accesses now.

* one more place we can reduce ref counts
2025-04-25 18:25:16 +02:00
Paulus Schoutsen ea90df434b Add an icon to the VoIP assist satellite entities (#143671) 2025-04-25 11:02:53 -05:00
Norbert Rittel 67fc682df2 Sentence-case "webhook" in locative (#143646) 2025-04-25 17:27:32 +02:00
Norbert Rittel 381b495efc Change "webhook (applet)" to lowercase in ifttt (#143642) 2025-04-25 17:27:22 +02:00
Norbert Rittel 812db815f1 Change "webhook" to lowercase and use "webhook service" in dialogflow (#143643)
* Change "webhook" to lowercase and fix grammar in `dialogflow`

* Replace "integration" with "service"
2025-04-25 17:22:12 +02:00
Retha Runolfsson 24ee19f1e2 Update quality scale for switchbot (#143145)
update quality_scale
2025-04-25 17:21:01 +02:00
Everton Leite f72c5ebb76 Add ratio attribute to Transmission torrent info (#143459) 2025-04-25 17:00:02 +02:00
epenet 1075ea1220 Bump renault-api to 0.3.0 (#143657) 2025-04-25 16:52:23 +02:00
Glenn Waters ce7edca136 Bump env_canada lib to 0.10.2 (#143664) 2025-04-25 16:44:16 +02:00
Doug Hoffman 3e16857a1e Bump uiprotect to 7.5.5 (#143668)
* Update manifest.json

* Update requirements_all.txt

* Update requirements_test_all.txt
2025-04-25 16:43:52 +02:00
Martin Hjelmare 5b1e32f51d Clean up Z-Wave config flow (#143670) 2025-04-25 16:43:19 +02:00
Ludovic BOUÉ 4adf5ce826 Support for Matter 1.4 Water Heater device type (#131505)
* Create water_heater.json

* Update water_heater.json

* Update water_heater.json

* TankVolume

* TankPercentage

* WaterHeaterMode

WaterHeaterMode

* Update sensor.py

* ruff-format

* Update water_heater.json

 Attributes of WaterHeaterManagement Cluster on Endpoint 2
ClusterId 148 (0x0094)

* Update test_sensor.py

water_heater fixture

* Update test_sensor.py

* SensorDeviceClass=VOLUME_STORAGE for `TankVolume`

* `BoostStateEnum` map

* WaterHeaterManagementBoostState

* Update sensor.py

* WaterHeaterManagementEstimatedHeatRequired

* Fix UnitOfEnergy

* Format

* Add `device_types.WaterHeater` to Climate

* Strings for Tank sensors

* WaterHeater icons

* Update icons.json

* Update strings.json

* Update water_heater.json

* ruff-format

* Fix tests

* Fix sensor.py

* Fix icons

* WaterHeaterManagementEstimatedHeatRequired

* WaterHeaterManagementBoostState

* BoostState as a binary sensor

* ElectricalPowerMeasurement values

* Fix tests

* Create water_heater.py

* Update climate.py from dev branch

* Resolve conflicts

* ruff-format

* Add Platform.WATER_HEATER

* Update water_heater.py

* Update water_heater.py

* Update water_heater.py

* Update water_heater.py

* Add WaterHeaterManagement sensors

* Update tests

* Add select test

* Add strings

* First try with water_heater

* Testing current_operation

* BoostState attribute

* target_temperature attributes

* target_temperature attribute

* set_temperature and set_operation_mode

* turn_on / turn_off

* Trigger Boost command

* Fix WaterHeaterBoostInfoStruct

* Add test file

* Add climate cluster to fixture

* Add climate cluster to fixture

* Add tests

* Add ON_OFF feature

* Update tests

* Update tests

* Translate WaterHeaterMode

* Change description

* Update test and snapshots

* Update snapshots

* Set entity name to None to make the device name be the name of the entity

* Format

* Update water_heater.py

* Fix format

* ruff-format

* Import ServiceValidationError

* Update homeassistant/components/matter/water_heater.py

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

* Update water_heater.py

* Update test_water_heater.py

* Update test_water_heater.ambr

* Update test_water_heater.py

* Update select.py

* Update snapshots

* Rename to boost_info

* Set WaterHeaterMode

* Update snapshots

* Update snapshots

* Fix for warning
W7431: Argument 3 should be of type AddConfigEntryEntitiesCallback in async_setup_entry (hass-argument-type)

* Update strings.json

* Update strings and tests

* Fix missing brace

* Update tests

* fix test

* Updates strings

* Fix async_set_temperature

* Update tests

* Update tests

* Update homeassistant/components/matter/water_heater.py

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

* Sort strings in strings.json

* Update homeassistant/components/matter/water_heater.py

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

* Remove unused line

* Remove min/max target temperatures

* Remove BOOST_STATE_MAP

* Add comment

* Remove SUPPORT_FLAGS_HEATER

* Remove system_mode_value check

* Update homeassistant/components/matter/water_heater.py

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

* Reformat async_set_temperature()

* Update snapshots

* Remove MatterWaterHeaterMode selector

* Update snapshots

* Rename test to test_water_heater_set_temperature

* Add test_water_heater_set_operation_mode

* Remove reset_mock

* Update tests/components/matter/test_water_heater.py

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

* Add test_update_from_water_heater

* Add test_water_heater_turn_on_off

* Add test_water_heater_boostmode

* Fix SystemMode value for STATE_HIGH_DEMAND

* Add disable boost from water heater device side test

* Remove unused lines

* Remove unused lines

* Fix test indentation

* Fix water heater tests

* Check for None

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2025-04-25 15:28:28 +02:00
Petro31 4a1905a2a2 Update template cover to modern style config (#141878) 2025-04-25 15:22:49 +02:00
Michael 59af3a396c Remove unnecessary mixins from AVM Fritz!SmartHome (#143658)
remove unnecessary mixin
2025-04-25 14:12:59 +02:00
Martin Hjelmare 7c584ece23 Make proper Z-Wave reconfigure flow (#143549)
* Make proper Z-Wave reconfigure flow

* Improve backup_failed string
2025-04-25 14:19:03 +03:00
Petro31 ff2c901930 Update trigger based template entity resolution order (#140660)
* Update trigger based template entity resolution order

* add test

* fix most comments

* Move resolution to base class

* add comment

* remove uncessary if statement

* add more tests

* update availability tests

* update logic stage 1

* phase 2 changes

* fix trigger template entity tests

* fix trigger template entities

* command line tests

* sql tests

* scrape test

* update doc string

* add rest tests

* update sql sensor _update signature

* fix scrape test constructor

* move state check to trigger_entity

* fix comments

* Update homeassistant/components/template/trigger_entity.py

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

* Update homeassistant/helpers/trigger_template_entity.py

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

* Update homeassistant/helpers/trigger_template_entity.py

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

* update command_line and rest

* update scrape

* update sql

* add case to command_line sensor

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
2025-04-25 13:17:25 +02:00
Erik Montnemery dc8e1773f1 Remove unused defaults from entity_registry.RegistryEntry (#143655) 2025-04-25 12:41:58 +02:00
Stefan Agner b0d9a2437d Bump aiohasupervisor from version 0.3.b1 to version 0.3.1 (#143585) 2025-04-25 12:20:28 +02:00
Paul Bottein 2be6ecd50f Assign plex update entity to server device (#143654)
* Assign plex update entity to server device

* Fix tests

* Apply suggestions from code review

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-25 11:21:14 +02:00
J. Nick Koston fa0bb35e6c Avoid creating tasks to add entities when no entities are passed (#143647)
async_add_entities would return early if no entities
were passed but its a bit cleaner to not create the
task in the first place. I noticed in py-spy that
tplink was passing empty lists frequently
which made a task and than did nothing.
2025-04-25 10:16:20 +02:00
Maciej Bieniek 5b503f21d7 Abort Shelly flows if the device is not fully provisioned (#143652)
* Abort flows if the device is not fully provisioned

* Update tests
2025-04-25 10:37:58 +03:00
J. Nick Koston cb0523660d Improve error logging when state is too long (#143636) 2025-04-24 18:37:32 -10:00
Michael 605bf7e287 Add volume flow rate device class to water_flow sensor in PEGELONLINE (#143631)
add SensorDeviceClass.VOLUME_FLOW_RATE to water_flow sensor
2025-04-25 00:42:58 +02:00
Michael 3405b2549b Add new units L/h , L/s and m³/s to volume flow rate sensor device class (#143625)
add new units L/h , L/s and m³/s
2025-04-25 00:17:47 +02:00
Shay Levy d83c617566 Fix naming consistency in Switcher service strings (#143629) 2025-04-25 01:00:42 +03:00
Brett Adams 7016c19b2f Disable polling for modern vehicles in Teslemetry (#143495) 2025-04-24 23:59:26 +02:00
Simone Chemelli 5cd4a0ced6 Use typed ConfigEntry in SamsungTV (#143627) 2025-04-24 23:55:10 +02:00
J. Nick Koston 347c1a2141 Remove duplicate _attr_should_poll in ESPHome EsphomeAssistSatelliteWakeWordSelect (#143624) 2025-04-25 00:41:51 +03:00
J. Nick Koston 46eae64ef6 Mark ESPHome quality as platinum (#143033) 2025-04-24 11:30:51 -10:00
J. Nick Koston a74fe60b91 Fix ESPHome async_step_reconfigure signature (#143620) 2025-04-24 11:30:27 -10:00
J. Nick Koston fab70a80bb Quality improvements for the ESPHome dashboard coordinator (#143619) 2025-04-24 23:20:05 +02:00
J. Nick Koston 2abe2f7d59 Remove unused hass from EsphomeAssistSatelliteWakeWordSelect (#143618) 2025-04-24 11:18:39 -10:00
Abílio Costa cc970354d7 Add Maytag virtual integration supported by Whirlpool (#143612) 2025-04-24 23:14:39 +02:00
Jan Bouwhuis e389ff2537 Allow float for device_tracker location accuracy (#143604) 2025-04-24 23:09:18 +02:00
hahn-th 088f0c82bd Bump homematicip to 2.0.1 (#143609) 2025-04-24 23:07:59 +02:00
Norbert Rittel fa1bb27dd2 Fix sentence-casing of "webhook" in gpslogger and geofency (#143614)
* Fix sentence-casing of "webhook" in `gpslogger`

* Fix sentence-casing of "webhook" in `geofency`
2025-04-25 00:07:42 +03:00
J. Nick Koston 5a6ce34352 Improve ESPHome test typing (#143617) 2025-04-24 10:41:37 -10:00
Paulus Schoutsen fdcb88977a Add voice styles to HA Cloud (#143605)
* Add voice styles to HA Cloud

* Add seperator and extract util
2025-04-24 16:23:15 -04:00
Stefan Agner a584ccb8f7 Remove add-on changelog from cached information (#143526) 2025-04-24 22:14:46 +02:00
Mick Vleeshouwer cc290b15f6 Fix available status of entities in Overkiz (#143538)
* Add availability property to OverkizEntity for device status

* Update homeassistant/components/overkiz/entity.py

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

---------

Co-authored-by: J. Nick Koston <nick+github@koston.org>
2025-04-24 21:58:36 +02:00
Shay Levy 575db4665d Fix Switcher review comments (#143607) 2025-04-24 21:54:25 +02:00
J. Nick Koston a61aff8432 Cleanup duplicate entry data in ESPHome assist_satellite (#143611) 2025-04-24 09:51:58 -10:00
J. Nick Koston 3aa1c60fe3 ESPHome quality improvements round 2 (#143613)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-04-24 09:51:33 -10:00
Abílio Costa 39f3aa7e78 Mark Whirlpool quality as bronze (#143603) 2025-04-24 20:44:15 +02:00
J. Nick Koston 01e2c3272b Improve error message when ESPHome reconfigure selects an unexpected device (#143608) 2025-04-24 08:44:02 -10:00
J. Nick Koston 5afcd3e54e Remove the previously deprecated ESPHome assist in progress binary sensor (#143536) 2025-04-24 08:43:48 -10:00
Norbert Rittel b081064954 Use correct singular and lowercase for "webhook" in mailgun (#143595) 2025-04-24 21:38:42 +03:00
Norbert Rittel 11e63ca96a Use correct singular and lowercase for "webhook" in twilio (#143596) 2025-04-24 21:38:03 +03:00
Abílio Costa 6457d46107 Raise ConfigEntryNotReady when fetching Whirlpool appliances fails (#143601) 2025-04-24 21:25:15 +03:00
Norbert Rittel 987bf4d850 Fix spelling of "counterclockwise" in deconz (#143523) 2025-04-24 21:23:40 +03:00
Paulus Schoutsen fa80c0a88d Bump hass-nabucasa to 0.96.0 (#143542)
* Bump hass-nabucasa to 0.96.0

* Adjust for new voice info format
2025-04-24 13:12:11 -04:00
Norbert Rittel f69484ba02 Fix missing plural on "Advisories" in environment_canada (#143562) 2025-04-24 19:17:30 +03:00
Norbert Rittel 11f63c7868 Use common strings for "already_in_progress" etc. in music_assistant (#143570)
* Use common string for "already_in_progress" in `music_assistant`

* Use common string for "cannot_connect" as well
2025-04-24 19:16:43 +03:00
Norbert Rittel 3245124553 Use common string for error::unknown in iometer (#143575) 2025-04-24 19:16:33 +03:00
Joost Lekkerkerker 44475967eb Bump pysmartthings to 3.0.5 (#143586) 2025-04-24 19:13:58 +03:00
Norbert Rittel 2d27b5ac53 Use common string for abort::unknown in srp_energy (#143576) 2025-04-24 17:20:53 +02:00
Erik Montnemery 2ae161d8b5 Wait for person integration in onboarding (#143584) 2025-04-24 17:08:53 +02:00
Norbert Rittel aefe83b1a3 Use common string for "cannot_connect" in imgw_pib (#143574) 2025-04-24 16:54:41 +02:00
Abílio Costa f86e85b931 Use None for Unknown state in Whirlpool sensor (#143582) 2025-04-24 15:12:45 +01:00
dependabot[bot] 993ebc9eba Bump github/codeql-action from 3.28.15 to 3.28.16 (#143546)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.28.15 to 3.28.16.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3.28.15...v3.28.16)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 3.28.16
  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-04-24 15:34:14 +02:00
dependabot[bot] 1d99bbf22e Bump actions/setup-python from 5.5.0 to 5.6.0 (#143545)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5.5.0 to 5.6.0.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5.5.0...v5.6.0)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: 5.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-24 15:32:11 +02:00
Åke Strandberg eb4fa635bf Add miele light platform (#143119)
* WIP

* Add light platform

* Address review comments

* Address review and improve tests

* Address review comments in tests
2025-04-24 15:02:39 +02:00
Michael 49522d93df Enable strict type checks for PEGELONLINE (#143563)
enable strict type checks for pegel_online
2025-04-24 14:42:47 +02:00
Norbert Rittel 9e0a7122f5 Fix typos and use a common string in synology_dsm (#143573)
- fix spelling of "Home Assistant", removing wrong hyphen
- remove excessive comma
- fix spelling of "passcode" (single word)
- capitalize "Zeroconf" (name)
- use common string for "reconfigure_successful"
2025-04-24 14:36:49 +02:00
Åke Strandberg e4fe7ba985 Fix bug in miele diagnostics (#143569)
Fix bug when redacting identifiers in diagnostics
2025-04-24 14:16:31 +02:00
Simone Chemelli f3ea11bbc1 Bump aiovodafone to 0.10.0 to use async_create_clientsession in Vodafone Station integration (#143537)
* Use async_create_clientsession in Vodafone Station integration

* bump library and rename method
2025-04-24 14:05:42 +02:00
Simone Chemelli 55de91530d Bump aiocomelit to 0.12.0 to use async_create_clientsession in Comelit integration (#143528)
* Use async_create_clientsession in Comelit integration

* bump library and rename method
2025-04-24 14:05:11 +02:00
Maciej Bieniek 290bbcfa3e Improve type annotation in the Shelly text and number platform (#143568)
* Define _id with type

* Define attribute_value with type
2025-04-24 13:55:40 +02:00
Maciej Bieniek 061a1be2bc Use DeviceInfo in the Shelly RPC entity base class (#143565)
Use DeviceInfo
2025-04-24 13:49:43 +02:00
Michael 4bd8c319dd Small fixes to the translation strings in PEGELONLINE (#143567)
small fixes
2025-04-24 13:47:23 +02:00
Michael 367022dd8c Use shorthand attributes in PEGELONLINE (#143564)
use shorthand attributes
2025-04-24 13:39:34 +02:00
ildar170975 f1975d9dbf Elevate Recorder "Error executing ..." from warning to error (#142816) 2025-04-24 11:36:39 +01:00
Retha Runolfsson 0764cf1165 Bump PySwitchbot to 0.60.1 (#143551) 2025-04-23 23:02:41 -10:00
406 changed files with 15560 additions and 4937 deletions
+5 -5
View File
@@ -32,7 +32,7 @@ jobs:
fetch-depth: 0
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -116,7 +116,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
if: needs.init.outputs.channel == 'dev'
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -175,7 +175,7 @@ jobs:
sed -i "s|pykrakenapi|# pykrakenapi|g" requirements_all.txt
- name: Download translations
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: translations
@@ -457,12 +457,12 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: translations
+20 -20
View File
@@ -249,7 +249,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -294,7 +294,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -334,7 +334,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -374,7 +374,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
id: python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
@@ -484,7 +484,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -587,7 +587,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -620,7 +620,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -677,7 +677,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -720,7 +720,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -767,7 +767,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -812,7 +812,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -889,7 +889,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -949,7 +949,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -968,7 +968,7 @@ jobs:
run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: pytest_buckets
- name: Compile English translations
@@ -1074,7 +1074,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1208,7 +1208,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1312,7 +1312,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1359,7 +1359,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ matrix.python-version }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ matrix.python-version }}
check-latest: true
@@ -1454,7 +1454,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
pattern: coverage-*
- name: Upload coverage to Codecov
@@ -1479,7 +1479,7 @@ jobs:
timeout-minutes: 10
steps:
- name: Download all coverage artifacts
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
pattern: test-results-*
- name: Upload test results to Codecov
+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.15
uses: github/codeql-action/init@v3.28.16
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.15
uses: github/codeql-action/analyze@v3.28.16
with:
category: "/language:python"
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
+8 -8
View File
@@ -36,7 +36,7 @@ jobs:
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@v5.5.0
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
check-latest: true
@@ -138,17 +138,17 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: requirements_diff
@@ -187,22 +187,22 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Download env_file
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: env_file
- name: Download build_constraints
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: build_constraints
- name: Download requirements_diff
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: requirements_diff
- name: Download requirements_all_wheels
uses: actions/download-artifact@v4.2.1
uses: actions/download-artifact@v4.3.0
with:
name: requirements_all_wheels
+1
View File
@@ -386,6 +386,7 @@ homeassistant.components.pandora.*
homeassistant.components.panel_custom.*
homeassistant.components.peblar.*
homeassistant.components.peco.*
homeassistant.components.pegel_online.*
homeassistant.components.persistent_notification.*
homeassistant.components.person.*
homeassistant.components.pi_hole.*
Generated
+4 -2
View File
@@ -1318,6 +1318,8 @@ build.json @home-assistant/supervisor
/tests/components/ruuvitag_ble/ @akx
/homeassistant/components/rympro/ @OnFreund @elad-bar @maorcc
/tests/components/rympro/ @OnFreund @elad-bar @maorcc
/homeassistant/components/s3/ @tomasbedrich
/tests/components/s3/ @tomasbedrich
/homeassistant/components/sabnzbd/ @shaiu @jpbede
/tests/components/sabnzbd/ @shaiu @jpbede
/homeassistant/components/saj/ @fredericvl
@@ -1439,8 +1441,8 @@ build.json @home-assistant/supervisor
/tests/components/solarlog/ @Ernst79 @dontinelli
/homeassistant/components/solax/ @squishykid @Darsstar
/tests/components/solax/ @squishykid @Darsstar
/homeassistant/components/soma/ @ratsept @sebfortier2288
/tests/components/soma/ @ratsept @sebfortier2288
/homeassistant/components/soma/ @ratsept
/tests/components/soma/ @ratsept
/homeassistant/components/sonarr/ @ctalkington
/tests/components/sonarr/ @ctalkington
/homeassistant/components/songpal/ @rytilahti @shenxn
+5
View File
@@ -75,6 +75,7 @@ from .core_config import async_process_ha_core_config
from .exceptions import HomeAssistantError
from .helpers import (
area_registry,
backup,
category_registry,
config_validation as cv,
device_registry,
@@ -880,6 +881,10 @@ async def _async_set_up_integrations(
if "recorder" in all_domains:
recorder.async_initialize_recorder(hass)
# Initialize backup
if "backup" in all_domains:
backup.async_initialize_backup(hass)
stages: list[tuple[str, set[str], int | None]] = [
*(
(name, domain_group, timeout)
+3 -6
View File
@@ -719,7 +719,7 @@ class LockCapabilities(AlexaEntity):
yield Alexa(self.entity)
@ENTITY_ADAPTERS.register(media_player.const.DOMAIN)
@ENTITY_ADAPTERS.register(media_player.DOMAIN)
class MediaPlayerCapabilities(AlexaEntity):
"""Class to represent MediaPlayer capabilities."""
@@ -757,9 +757,7 @@ class MediaPlayerCapabilities(AlexaEntity):
if supported & media_player.MediaPlayerEntityFeature.SELECT_SOURCE:
inputs = AlexaInputController.get_valid_inputs(
self.entity.attributes.get(
media_player.const.ATTR_INPUT_SOURCE_LIST, []
)
self.entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST, [])
)
if len(inputs) > 0:
yield AlexaInputController(self.entity)
@@ -776,8 +774,7 @@ class MediaPlayerCapabilities(AlexaEntity):
and domain != "denonavr"
):
inputs = AlexaEqualizerController.get_valid_inputs(
self.entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
or []
self.entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST) or []
)
if len(inputs) > 0:
yield AlexaEqualizerController(self.entity)
+12 -14
View File
@@ -566,7 +566,7 @@ async def async_api_set_volume(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
@@ -589,7 +589,7 @@ async def async_api_select_input(
# Attempt to map the ALL UPPERCASE payload name to a source.
# Strips trailing 1 to match single input devices.
source_list = entity.attributes.get(media_player.const.ATTR_INPUT_SOURCE_LIST) or []
source_list = entity.attributes.get(media_player.ATTR_INPUT_SOURCE_LIST) or []
for source in source_list:
formatted_source = (
source.lower().replace("-", "").replace("_", "").replace(" ", "")
@@ -611,7 +611,7 @@ async def async_api_select_input(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_INPUT_SOURCE: media_input,
media_player.ATTR_INPUT_SOURCE: media_input,
}
await hass.services.async_call(
@@ -636,7 +636,7 @@ async def async_api_adjust_volume(
volume_delta = int(directive.payload["volume"])
entity = directive.entity
current_level = entity.attributes[media_player.const.ATTR_MEDIA_VOLUME_LEVEL]
current_level = entity.attributes[media_player.ATTR_MEDIA_VOLUME_LEVEL]
# read current state
try:
@@ -648,7 +648,7 @@ async def async_api_adjust_volume(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_LEVEL: volume,
media_player.ATTR_MEDIA_VOLUME_LEVEL: volume,
}
await hass.services.async_call(
@@ -709,7 +709,7 @@ async def async_api_set_mute(
entity = directive.entity
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_VOLUME_MUTED: mute,
media_player.ATTR_MEDIA_VOLUME_MUTED: mute,
}
await hass.services.async_call(
@@ -1708,15 +1708,13 @@ async def async_api_changechannel(
data: dict[str, Any] = {
ATTR_ENTITY_ID: entity.entity_id,
media_player.const.ATTR_MEDIA_CONTENT_ID: channel,
media_player.const.ATTR_MEDIA_CONTENT_TYPE: (
media_player.const.MEDIA_TYPE_CHANNEL
),
media_player.ATTR_MEDIA_CONTENT_ID: channel,
media_player.ATTR_MEDIA_CONTENT_TYPE: (media_player.MediaType.CHANNEL),
}
await hass.services.async_call(
entity.domain,
media_player.const.SERVICE_PLAY_MEDIA,
media_player.SERVICE_PLAY_MEDIA,
data,
blocking=False,
context=context,
@@ -1825,13 +1823,13 @@ async def async_api_set_eq_mode(
context: ha.Context,
) -> AlexaResponse:
"""Process a SetMode request for EqualizerController."""
mode = directive.payload["mode"]
mode: str = directive.payload["mode"]
entity = directive.entity
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
sound_mode_list = entity.attributes.get(media_player.const.ATTR_SOUND_MODE_LIST)
sound_mode_list = entity.attributes.get(media_player.ATTR_SOUND_MODE_LIST)
if sound_mode_list and mode.lower() in sound_mode_list:
data[media_player.const.ATTR_SOUND_MODE] = mode.lower()
data[media_player.ATTR_SOUND_MODE] = mode.lower()
else:
msg = f"failed to map sound mode {mode} to a mode on {entity.entity_id}"
raise AlexaInvalidValueError(msg)
@@ -9,11 +9,13 @@ from anthropic import AsyncStream
from anthropic._types import NOT_GIVEN
from anthropic.types import (
InputJSONDelta,
MessageDeltaUsage,
MessageParam,
MessageStreamEvent,
RawContentBlockDeltaEvent,
RawContentBlockStartEvent,
RawContentBlockStopEvent,
RawMessageDeltaEvent,
RawMessageStartEvent,
RawMessageStopEvent,
RedactedThinkingBlock,
@@ -31,6 +33,7 @@ from anthropic.types import (
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
Usage,
)
from voluptuous_openapi import convert
@@ -162,7 +165,8 @@ def _convert_content(
return messages
async def _transform_stream(
async def _transform_stream( # noqa: C901 - This is complex, but better to have it in one place
chat_log: conversation.ChatLog,
result: AsyncStream[MessageStreamEvent],
messages: list[MessageParam],
) -> AsyncGenerator[conversation.AssistantContentDeltaDict]:
@@ -207,6 +211,7 @@ async def _transform_stream(
| None
) = None
current_tool_args: str
input_usage: Usage | None = None
async for response in result:
LOGGER.debug("Received response: %s", response)
@@ -215,6 +220,7 @@ async def _transform_stream(
if response.message.role != "assistant":
raise ValueError("Unexpected message role")
current_message = MessageParam(role=response.message.role, content=[])
input_usage = response.message.usage
elif isinstance(response, RawContentBlockStartEvent):
if isinstance(response.content_block, ToolUseBlock):
current_block = ToolUseBlockParam(
@@ -285,12 +291,34 @@ async def _transform_stream(
raise ValueError("Unexpected stop event without a current message")
current_message["content"].append(current_block) # type: ignore[union-attr]
current_block = None
elif isinstance(response, RawMessageDeltaEvent):
if (usage := response.usage) is not None:
chat_log.async_trace(_create_token_stats(input_usage, usage))
elif isinstance(response, RawMessageStopEvent):
if current_message is not None:
messages.append(current_message)
current_message = None
def _create_token_stats(
input_usage: Usage | None, response_usage: MessageDeltaUsage
) -> dict[str, Any]:
"""Create token stats for conversation agent tracing."""
input_tokens = 0
cached_input_tokens = 0
if input_usage:
input_tokens = input_usage.input_tokens
cached_input_tokens = input_usage.cache_creation_input_tokens or 0
output_tokens = response_usage.output_tokens
return {
"stats": {
"input_tokens": input_tokens,
"cached_input_tokens": cached_input_tokens,
"output_tokens": output_tokens,
}
}
class AnthropicConversationEntity(
conversation.ConversationEntity, conversation.AbstractConversationAgent
):
@@ -393,7 +421,8 @@ class AnthropicConversationEntity(
[
content
async for content in chat_log.async_add_delta_content_stream(
user_input.agent_id, _transform_stream(stream, messages)
user_input.agent_id,
_transform_stream(chat_log, stream, messages),
)
if not isinstance(content, conversation.AssistantContent)
]
@@ -113,4 +113,7 @@ class APCUPSdCoordinator(DataUpdateCoordinator[APCUPSdData]):
data = await aioapcaccess.request_status(self._host, self._port)
return APCUPSdData(data)
except (OSError, asyncio.IncompleteReadError) as error:
raise UpdateFailed(error) from error
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
) from error
@@ -219,5 +219,10 @@
"name": "Transfer to battery"
}
}
},
"exceptions": {
"cannot_connect": {
"message": "Cannot connect to APC UPS Daemon."
}
}
}
+11 -16
View File
@@ -2,9 +2,9 @@
from homeassistant.config_entries import SOURCE_SYSTEM
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.helpers import config_validation as cv, discovery_flow
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.typing import ConfigType
@@ -36,6 +36,7 @@ from .manager import (
IdleEvent,
IncorrectPasswordError,
ManagerBackup,
ManagerStateEvent,
NewBackup,
RestoreBackupEvent,
RestoreBackupStage,
@@ -68,12 +69,12 @@ __all__ = [
"IncorrectPasswordError",
"LocalBackupAgent",
"ManagerBackup",
"ManagerStateEvent",
"NewBackup",
"RestoreBackupEvent",
"RestoreBackupStage",
"RestoreBackupState",
"WrittenBackup",
"async_get_manager",
"suggested_filename",
"suggested_filename_from_name_date",
]
@@ -98,7 +99,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
backup_manager = BackupManager(hass, reader_writer)
hass.data[DATA_MANAGER] = backup_manager
await backup_manager.async_setup()
try:
await backup_manager.async_setup()
except Exception as err:
hass.data[DATA_BACKUP].manager_ready.set_exception(err)
raise
else:
hass.data[DATA_BACKUP].manager_ready.set_result(None)
async_register_websocket_handlers(hass, with_hassio)
@@ -153,15 +160,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: BackupConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
@callback
def async_get_manager(hass: HomeAssistant) -> BackupManager:
"""Get the backup manager instance.
Raises HomeAssistantError if the backup integration is not available.
"""
if DATA_MANAGER not in hass.data:
raise HomeAssistantError("Backup integration is not available")
return hass.data[DATA_MANAGER]
@@ -0,0 +1,38 @@
"""Websocket commands for the Backup integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import async_subscribe_events
from .const import DATA_MANAGER
from .manager import ManagerStateEvent
@callback
def async_register_websocket_handlers(hass: HomeAssistant) -> None:
"""Register websocket commands."""
websocket_api.async_register_command(hass, handle_subscribe_events)
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
if DATA_MANAGER in hass.data:
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = async_subscribe_events(hass, on_event)
connection.send_result(msg["id"])
@@ -8,6 +8,10 @@ from datetime import datetime
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.backup import (
async_subscribe_events,
async_subscribe_platform_events,
)
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN, LOGGER
@@ -50,8 +54,8 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
update_interval=None,
)
self.unsubscribe: list[Callable[[], None]] = [
backup_manager.async_subscribe_events(self._on_event),
backup_manager.async_subscribe_platform_events(self._on_event),
async_subscribe_events(hass, self._on_event),
async_subscribe_platform_events(hass, self._on_event),
]
self.backup_manager = backup_manager
+7 -30
View File
@@ -36,6 +36,7 @@ from homeassistant.helpers import (
issue_registry as ir,
start,
)
from homeassistant.helpers.backup import DATA_BACKUP
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util, json as json_util
@@ -358,10 +359,12 @@ class BackupManager:
# Latest backup event and backup event subscribers
self.last_event: ManagerStateEvent = BlockedEvent()
self.last_action_event: ManagerStateEvent | None = None
self._backup_event_subscriptions: list[Callable[[ManagerStateEvent], None]] = []
self._backup_platform_event_subscriptions: list[
Callable[[BackupPlatformEvent], None]
] = []
self._backup_event_subscriptions = hass.data[
DATA_BACKUP
].backup_event_subscriptions
self._backup_platform_event_subscriptions = hass.data[
DATA_BACKUP
].backup_platform_event_subscriptions
async def async_setup(self) -> None:
"""Set up the backup manager."""
@@ -1351,32 +1354,6 @@ class BackupManager:
for subscription in self._backup_event_subscriptions:
subscription(event)
@callback
def async_subscribe_events(
self,
on_event: Callable[[ManagerStateEvent], None],
) -> Callable[[], None]:
"""Subscribe events."""
def remove_subscription() -> None:
self._backup_event_subscriptions.remove(on_event)
self._backup_event_subscriptions.append(on_event)
return remove_subscription
@callback
def async_subscribe_platform_events(
self,
on_event: Callable[[BackupPlatformEvent], None],
) -> Callable[[], None]:
"""Subscribe to backup platform events."""
def remove_subscription() -> None:
self._backup_platform_event_subscriptions.remove(on_event)
self._backup_platform_event_subscriptions.append(on_event)
return remove_subscription
def _update_issue_backup_failed(self) -> None:
"""Update issue registry when a backup fails."""
ir.async_create_issue(
@@ -19,14 +19,9 @@ from homeassistant.components.onboarding import (
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from . import (
BackupManager,
Folder,
IncorrectPasswordError,
async_get_manager,
http as backup_http,
)
from . import BackupManager, Folder, IncorrectPasswordError, http as backup_http
if TYPE_CHECKING:
from homeassistant.components.onboarding import OnboardingStoreData
@@ -59,7 +54,7 @@ def with_backup_manager[_ViewT: BaseOnboardingView, **_P](
if self._data["done"]:
raise HTTPUnauthorized
manager = async_get_manager(request.app[KEY_HASS])
manager = await async_get_backup_manager(request.app[KEY_HASS])
return await func(self, manager, request, *args, **kwargs)
return with_backup
+1 -25
View File
@@ -10,11 +10,7 @@ from homeassistant.helpers import config_validation as cv
from .config import Day, ScheduleRecurrence
from .const import DATA_MANAGER, LOGGER
from .manager import (
DecryptOnDowloadNotSupported,
IncorrectPasswordError,
ManagerStateEvent,
)
from .manager import DecryptOnDowloadNotSupported, IncorrectPasswordError
from .models import BackupNotFound, Folder
@@ -34,7 +30,6 @@ def async_register_websocket_handlers(hass: HomeAssistant, with_hassio: bool) ->
websocket_api.async_register_command(hass, handle_create_with_automatic_settings)
websocket_api.async_register_command(hass, handle_delete)
websocket_api.async_register_command(hass, handle_restore)
websocket_api.async_register_command(hass, handle_subscribe_events)
websocket_api.async_register_command(hass, handle_config_info)
websocket_api.async_register_command(hass, handle_config_update)
@@ -401,22 +396,3 @@ def handle_config_update(
changes.pop("type")
manager.config.update(**changes)
connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command({vol.Required("type"): "backup/subscribe_events"})
@websocket_api.async_response
async def handle_subscribe_events(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Subscribe to backup events."""
def on_event(event: ManagerStateEvent) -> None:
connection.send_message(websocket_api.event_message(msg["id"], event))
manager = hass.data[DATA_MANAGER]
on_event(manager.last_event)
connection.subscriptions[msg["id"]] = manager.async_subscribe_events(on_event)
connection.send_result(msg["id"])
@@ -19,8 +19,8 @@
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.5",
"bluetooth-data-tools==1.27.0",
"bluetooth-data-tools==1.28.0",
"dbus-fast==2.43.0",
"habluetooth==3.39.0"
"habluetooth==3.42.0"
]
}
+2
View File
@@ -93,3 +93,5 @@ STT_ENTITY_UNIQUE_ID = "cloud-speech-to-text"
TTS_ENTITY_UNIQUE_ID = "cloud-text-to-speech"
LOGIN_MFA_TIMEOUT = 60
VOICE_STYLE_SEPERATOR = "||"
+35 -12
View File
@@ -18,7 +18,7 @@ from aiohttp import web
import attr
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
from hass_nabucasa.const import STATE_DISCONNECTED
from hass_nabucasa.voice import TTS_VOICES
from hass_nabucasa.voice_data import TTS_VOICES
import voluptuous as vol
from homeassistant.components import websocket_api
@@ -57,6 +57,7 @@ from .const import (
PREF_REMOTE_ALLOW_REMOTE_ENABLE,
PREF_TTS_DEFAULT_VOICE,
REQUEST_TIMEOUT,
VOICE_STYLE_SEPERATOR,
)
from .google_config import CLOUD_GOOGLE
from .repairs import async_manage_legacy_subscription_issue
@@ -591,10 +592,21 @@ async def websocket_subscription(
def validate_language_voice(value: tuple[str, str]) -> tuple[str, str]:
"""Validate language and voice."""
language, voice = value
style: str | None
voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
if not style:
style = None
if language not in TTS_VOICES:
raise vol.Invalid(f"Invalid language {language}")
if voice not in TTS_VOICES[language]:
if voice not in (language_info := TTS_VOICES[language]):
raise vol.Invalid(f"Invalid voice {voice} for language {language}")
voice_info = language_info[voice]
if style and (
isinstance(voice_info, str) or style not in voice_info.get("variants", [])
):
raise vol.Invalid(
f"Invalid style {style} for voice {voice} in language {language}"
)
return value
@@ -1012,13 +1024,24 @@ def tts_info(
msg: dict[str, Any],
) -> None:
"""Fetch available tts info."""
connection.send_result(
msg["id"],
{
"languages": [
(language, voice)
for language, voices in TTS_VOICES.items()
for voice in voices
]
},
)
result = []
for language, voices in TTS_VOICES.items():
for voice_id, voice_info in voices.items():
if isinstance(voice_info, str):
result.append((language, voice_id, voice_info))
continue
name = voice_info["name"]
result.append((language, voice_id, name))
result.extend(
[
(
language,
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
f"{name} ({variant})",
)
for variant in voice_info.get("variants", [])
]
)
connection.send_result(msg["id"], {"languages": result})
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.94.0"],
"requirements": ["hass-nabucasa==0.96.0"],
"single_config_entry": true
}
+135 -46
View File
@@ -6,7 +6,8 @@ import logging
from typing import Any
from hass_nabucasa import Cloud
from hass_nabucasa.voice import MAP_VOICE, TTS_VOICES, AudioOutput, Gender, VoiceError
from hass_nabucasa.voice import MAP_VOICE, AudioOutput, Gender, VoiceError
from hass_nabucasa.voice_data import TTS_VOICES
import voluptuous as vol
from homeassistant.components.tts import (
@@ -30,7 +31,13 @@ from homeassistant.setup import async_when_setup
from .assist_pipeline import async_migrate_cloud_pipeline_engine
from .client import CloudClient
from .const import DATA_CLOUD, DATA_PLATFORMS_SETUP, DOMAIN, TTS_ENTITY_UNIQUE_ID
from .const import (
DATA_CLOUD,
DATA_PLATFORMS_SETUP,
DOMAIN,
TTS_ENTITY_UNIQUE_ID,
VOICE_STYLE_SEPERATOR,
)
from .prefs import CloudPreferences
ATTR_GENDER = "gender"
@@ -57,6 +64,7 @@ DEFAULT_VOICES = {
"ar-SY": "AmanyNeural",
"ar-TN": "ReemNeural",
"ar-YE": "MaryamNeural",
"as-IN": "PriyomNeural",
"az-AZ": "BabekNeural",
"bg-BG": "KalinaNeural",
"bn-BD": "NabanitaNeural",
@@ -126,6 +134,8 @@ DEFAULT_VOICES = {
"id-ID": "GadisNeural",
"is-IS": "GudrunNeural",
"it-IT": "ElsaNeural",
"iu-Cans-CA": "SiqiniqNeural",
"iu-Latn-CA": "SiqiniqNeural",
"ja-JP": "NanamiNeural",
"jv-ID": "SitiNeural",
"ka-GE": "EkaNeural",
@@ -147,6 +157,8 @@ DEFAULT_VOICES = {
"ne-NP": "HemkalaNeural",
"nl-BE": "DenaNeural",
"nl-NL": "ColetteNeural",
"or-IN": "SubhasiniNeural",
"pa-IN": "OjasNeural",
"pl-PL": "AgnieszkaNeural",
"ps-AF": "LatifaNeural",
"pt-BR": "FranciscaNeural",
@@ -158,6 +170,7 @@ DEFAULT_VOICES = {
"sl-SI": "PetraNeural",
"so-SO": "UbaxNeural",
"sq-AL": "AnilaNeural",
"sr-Latn-RS": "NicholasNeural",
"sr-RS": "SophieNeural",
"su-ID": "TutiNeural",
"sv-SE": "SofieNeural",
@@ -177,12 +190,9 @@ DEFAULT_VOICES = {
"vi-VN": "HoaiMyNeural",
"wuu-CN": "XiaotongNeural",
"yue-CN": "XiaoMinNeural",
"zh-CN": "XiaoxiaoNeural",
"zh-CN-henan": "YundengNeural",
"zh-CN-liaoning": "XiaobeiNeural",
"zh-CN-shaanxi": "XiaoniNeural",
"zh-CN-shandong": "YunxiangNeural",
"zh-CN-sichuan": "YunxiNeural",
"zh-CN": "XiaoxiaoNeural",
"zh-HK": "HiuMaanNeural",
"zh-TW": "HsiaoChenNeural",
"zu-ZA": "ThandoNeural",
@@ -191,6 +201,39 @@ DEFAULT_VOICES = {
_LOGGER = logging.getLogger(__name__)
@callback
def _prepare_voice_args(
*,
hass: HomeAssistant,
language: str,
voice: str,
gender: str | None,
) -> dict:
"""Prepare voice arguments."""
gender = handle_deprecated_gender(hass, gender)
style: str | None
original_voice, _, style = voice.partition(VOICE_STYLE_SEPERATOR)
if not style:
style = None
updated_voice = handle_deprecated_voice(hass, original_voice)
if updated_voice not in TTS_VOICES[language]:
default_voice = DEFAULT_VOICES[language]
_LOGGER.debug(
"Unsupported voice %s detected, falling back to default %s for %s",
voice,
default_voice,
language,
)
updated_voice = default_voice
return {
"language": language,
"voice": updated_voice,
"gender": gender,
"style": style,
}
def _deprecated_platform(value: str) -> str:
"""Validate if platform is deprecated."""
if value == DOMAIN:
@@ -328,36 +371,59 @@ class CloudTTSEntity(TextToSpeechEntity):
"""Return a list of supported voices for a language."""
if not (voices := TTS_VOICES.get(language)):
return None
return [Voice(voice, voice) for voice in voices]
result = []
for voice_id, voice_info in voices.items():
if isinstance(voice_info, str):
result.append(
Voice(
voice_id,
voice_info,
)
)
continue
name = voice_info["name"]
result.append(
Voice(
voice_id,
name,
)
)
result.extend(
[
Voice(
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
f"{name} ({variant})",
)
for variant in voice_info.get("variants", [])
]
)
return result
async def async_get_tts_audio(
self, message: str, language: str, options: dict[str, Any]
) -> TtsAudioType:
"""Load TTS from Home Assistant Cloud."""
gender: Gender | str | None = options.get(ATTR_GENDER)
gender = handle_deprecated_gender(self.hass, gender)
original_voice: str = options.get(
ATTR_VOICE,
self._voice if language == self._language else DEFAULT_VOICES[language],
)
voice = handle_deprecated_voice(self.hass, original_voice)
if voice not in TTS_VOICES[language]:
default_voice = DEFAULT_VOICES[language]
_LOGGER.debug(
"Unsupported voice %s detected, falling back to default %s for %s",
voice,
default_voice,
language,
)
voice = default_voice
# Process TTS
try:
data = await self.cloud.voice.process_tts(
text=message,
language=language,
gender=gender,
voice=voice,
output=options[ATTR_AUDIO_OUTPUT],
**_prepare_voice_args(
hass=self.hass,
language=language,
voice=options.get(
ATTR_VOICE,
self._voice
if language == self._language
else DEFAULT_VOICES[language],
),
gender=options.get(ATTR_GENDER),
),
)
except VoiceError as err:
_LOGGER.error("Voice error: %s", err)
@@ -401,7 +467,38 @@ class CloudProvider(Provider):
"""Return a list of supported voices for a language."""
if not (voices := TTS_VOICES.get(language)):
return None
return [Voice(voice, voice) for voice in voices]
result = []
for voice_id, voice_info in voices.items():
if isinstance(voice_info, str):
result.append(
Voice(
voice_id,
voice_info,
)
)
continue
name = voice_info["name"]
result.append(
Voice(
voice_id,
name,
)
)
result.extend(
[
Voice(
f"{voice_id}{VOICE_STYLE_SEPERATOR}{variant}",
f"{name} ({variant})",
)
for variant in voice_info.get("variants", [])
]
)
return result
@property
def default_options(self) -> dict[str, str]:
@@ -415,30 +512,22 @@ class CloudProvider(Provider):
) -> TtsAudioType:
"""Load TTS from Home Assistant Cloud."""
assert self.hass is not None
gender: Gender | str | None = options.get(ATTR_GENDER)
gender = handle_deprecated_gender(self.hass, gender)
original_voice: str = options.get(
ATTR_VOICE,
self._voice if language == self._language else DEFAULT_VOICES[language],
)
voice = handle_deprecated_voice(self.hass, original_voice)
if voice not in TTS_VOICES[language]:
default_voice = DEFAULT_VOICES[language]
_LOGGER.debug(
"Unsupported voice %s detected, falling back to default %s for %s",
voice,
default_voice,
language,
)
voice = default_voice
# Process TTS
try:
data = await self.cloud.voice.process_tts(
text=message,
language=language,
gender=gender,
voice=voice,
output=options[ATTR_AUDIO_OUTPUT],
**_prepare_voice_args(
hass=self.hass,
language=language,
voice=options.get(
ATTR_VOICE,
self._voice
if language == self._language
else DEFAULT_VOICES[language],
),
gender=options.get(ATTR_GENDER),
),
)
except VoiceError as err:
_LOGGER.error("Voice error: %s", err)
@@ -12,6 +12,7 @@ from .coordinator import (
ComelitSerialBridge,
ComelitVedoSystem,
)
from .utils import async_client_session
BRIDGE_PLATFORMS = [
Platform.CLIMATE,
@@ -32,6 +33,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
"""Set up Comelit platform."""
coordinator: ComelitBaseCoordinator
session = await async_client_session(hass)
if entry.data.get(CONF_TYPE, BRIDGE) == BRIDGE:
coordinator = ComelitSerialBridge(
hass,
@@ -39,6 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
entry.data[CONF_HOST],
entry.data.get(CONF_PORT, DEFAULT_PORT),
entry.data[CONF_PIN],
session,
)
platforms = BRIDGE_PLATFORMS
else:
@@ -48,6 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ComelitConfigEntry) -> b
entry.data[CONF_HOST],
entry.data.get(CONF_PORT, DEFAULT_PORT),
entry.data[CONF_PIN],
session,
)
platforms = VEDO_PLATFORMS
@@ -22,6 +22,7 @@ from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from .const import _LOGGER, DEFAULT_PORT, DEVICE_TYPE_LIST, DOMAIN
from .utils import async_client_session
DEFAULT_HOST = "192.168.1.252"
DEFAULT_PIN = 111111
@@ -47,10 +48,14 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
"""Validate the user input allows us to connect."""
api: ComelitCommonApi
session = await async_client_session(hass)
if data.get(CONF_TYPE, BRIDGE) == BRIDGE:
api = ComeliteSerialBridgeApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
api = ComeliteSerialBridgeApi(
data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session
)
else:
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN])
api = ComelitVedoApi(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], session)
try:
await api.login()
@@ -15,6 +15,7 @@ from aiocomelit.api import (
)
from aiocomelit.const import BRIDGE, VEDO
from aiocomelit.exceptions import CannotAuthenticate, CannotConnect, CannotRetrieveData
from aiohttp import ClientSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -95,9 +96,16 @@ class ComelitBaseCoordinator(DataUpdateCoordinator[T]):
await self.api.login()
return await self._async_update_system_data()
except (CannotConnect, CannotRetrieveData) as err:
raise UpdateFailed(repr(err)) from err
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": repr(err)},
) from err
except CannotAuthenticate as err:
raise ConfigEntryAuthFailed from err
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="cannot_authenticate",
) from err
@abstractmethod
async def _async_update_system_data(self) -> T:
@@ -119,9 +127,10 @@ class ComelitSerialBridge(
host: str,
port: int,
pin: int,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
self.api = ComeliteSerialBridgeApi(host, port, pin)
self.api = ComeliteSerialBridgeApi(host, port, pin, session)
super().__init__(hass, entry, BRIDGE, host)
async def _async_update_system_data(
@@ -144,9 +153,10 @@ class ComelitVedoSystem(ComelitBaseCoordinator[AlarmDataObject]):
host: str,
port: int,
pin: int,
session: ClientSession,
) -> None:
"""Initialize the scanner."""
self.api = ComelitVedoApi(host, port, pin)
self.api = ComelitVedoApi(host, port, pin, session)
super().__init__(hass, entry, VEDO, host)
async def _async_update_system_data(
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "bronze",
"requirements": ["aiocomelit==0.11.3"]
"requirements": ["aiocomelit==0.12.0"]
}
@@ -70,9 +70,7 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations:
status: todo
comment: PR in progress
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: todo
@@ -86,7 +84,5 @@ rules:
# Platinum
async-dependency: done
inject-websession:
status: todo
comment: implement aiohttp_client.async_create_clientsession
inject-websession: done
strict-typing: done
@@ -74,7 +74,10 @@
"message": "Error connecting: {error}"
},
"cannot_authenticate": {
"message": "Error authenticating: {error}"
"message": "Error authenticating"
},
"updated_failed": {
"message": "Failed to update data: {error}"
}
}
}
+13
View File
@@ -0,0 +1,13 @@
"""Utils for Comelit."""
from aiohttp import ClientSession, CookieJar
from homeassistant.core import HomeAssistant
from homeassistant.helpers import aiohttp_client
async def async_client_session(hass: HomeAssistant) -> ClientSession:
"""Return a new aiohttp session."""
return aiohttp_client.async_create_clientsession(
hass, verify_ssl=False, cookie_jar=CookieJar(unsafe=True)
)
@@ -56,7 +56,10 @@ from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.entity_platform import async_get_platforms
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.service import async_register_admin_service
from homeassistant.helpers.trigger_template_entity import CONF_AVAILABILITY
from homeassistant.helpers.trigger_template_entity import (
CONF_AVAILABILITY,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType
from .const import (
@@ -91,7 +94,9 @@ BINARY_SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_PAYLOAD_OFF, default=DEFAULT_PAYLOAD_OFF): cv.string,
vol.Optional(CONF_PAYLOAD_ON, default=DEFAULT_PAYLOAD_ON): cv.string,
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(
@@ -108,7 +113,9 @@ COVER_SCHEMA = vol.Schema(
vol.Optional(CONF_COMMAND_STOP, default="true"): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_UNIQUE_ID): cv.string,
@@ -134,7 +141,9 @@ SENSOR_SCHEMA = vol.Schema(
vol.Optional(CONF_NAME, default=SENSOR_DEFAULT_NAME): cv.string,
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_UNIQUE_ID): cv.string,
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
vol.Optional(CONF_STATE_CLASS): SENSOR_STATE_CLASSES_SCHEMA,
@@ -150,7 +159,9 @@ SWITCH_SCHEMA = vol.Schema(
vol.Optional(CONF_COMMAND_ON, default="true"): cv.string,
vol.Optional(CONF_COMMAND_STATE): cv.string,
vol.Required(CONF_NAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): vol.All(
cv.template, ValueTemplate.from_template
),
vol.Optional(CONF_ICON): cv.template,
vol.Optional(CONF_COMMAND_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int,
vol.Optional(CONF_UNIQUE_ID): cv.string,
@@ -18,7 +18,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
@@ -50,7 +53,7 @@ async def async_setup_platform(
scan_interval: timedelta = binary_sensor_config.get(
CONF_SCAN_INTERVAL, SCAN_INTERVAL
)
value_template: Template | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
value_template: ValueTemplate | None = binary_sensor_config.get(CONF_VALUE_TEMPLATE)
data = CommandSensorData(hass, command, command_timeout)
@@ -86,7 +89,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
config: ConfigType,
payload_on: str,
payload_off: str,
value_template: Template | None,
value_template: ValueTemplate | None,
scan_interval: timedelta,
) -> None:
"""Initialize the Command line binary sensor."""
@@ -133,9 +136,14 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
await self.data.async_update()
value = self.data.value
variables = self._template_variables_with_value(value)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._value_template is not None:
value = self._value_template.async_render_with_possible_json_value(
value, None
value = self._value_template.async_render_as_value_template(
self.entity_id, variables, None
)
self._attr_is_on = None
if value == self._payload_on:
@@ -143,7 +151,7 @@ class CommandBinarySensor(ManualTriggerEntity, BinarySensorEntity):
elif value == self._payload_off:
self._attr_is_on = False
self._process_manual_data(value)
self._process_manual_data(variables)
self.async_write_ha_state()
async def async_update(self) -> None:
+14 -5
View File
@@ -20,7 +20,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util, slugify
@@ -79,7 +82,7 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
command_close: str,
command_stop: str,
command_state: str | None,
value_template: Template | None,
value_template: ValueTemplate | None,
timeout: int,
scan_interval: timedelta,
) -> None:
@@ -164,14 +167,20 @@ class CommandCover(ManualTriggerEntity, CoverEntity):
"""Update device state."""
if self._command_state:
payload = str(await self._async_query_state())
variables = self._template_variables_with_value(payload)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._value_template:
payload = self._value_template.async_render_with_possible_json_value(
payload, None
payload = self._value_template.async_render_as_value_template(
self.entity_id, variables, None
)
self._state = None
if payload:
self._state = int(payload)
self._process_manual_data(payload)
self._process_manual_data(variables)
self.async_write_ha_state()
async def async_update(self) -> None:
@@ -23,7 +23,10 @@ from homeassistant.exceptions import TemplateError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerSensorEntity
from homeassistant.helpers.trigger_template_entity import (
ManualTriggerSensorEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util
@@ -57,7 +60,7 @@ async def async_setup_platform(
json_attributes: list[str] | None = sensor_config.get(CONF_JSON_ATTRIBUTES)
json_attributes_path: str | None = sensor_config.get(CONF_JSON_ATTRIBUTES_PATH)
scan_interval: timedelta = sensor_config.get(CONF_SCAN_INTERVAL, SCAN_INTERVAL)
value_template: Template | None = sensor_config.get(CONF_VALUE_TEMPLATE)
value_template: ValueTemplate | None = sensor_config.get(CONF_VALUE_TEMPLATE)
data = CommandSensorData(hass, command, command_timeout)
trigger_entity_config = {
@@ -88,7 +91,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self,
data: CommandSensorData,
config: ConfigType,
value_template: Template | None,
value_template: ValueTemplate | None,
json_attributes: list[str] | None,
json_attributes_path: str | None,
scan_interval: timedelta,
@@ -144,6 +147,11 @@ class CommandSensor(ManualTriggerSensorEntity):
await self.data.async_update()
value = self.data.value
variables = self._template_variables_with_value(self.data.value)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
if self._json_attributes:
self._attr_extra_state_attributes = {}
if value:
@@ -168,16 +176,17 @@ class CommandSensor(ManualTriggerSensorEntity):
LOGGER.warning("Unable to parse output as JSON: %s", value)
else:
LOGGER.warning("Empty reply found when expecting JSON data")
if self._value_template is None:
self._attr_native_value = None
self._process_manual_data(value)
self._process_manual_data(variables)
self.async_write_ha_state()
return
self._attr_native_value = None
if self._value_template is not None and value is not None:
value = self._value_template.async_render_with_possible_json_value(
value,
None,
value = self._value_template.async_render_as_value_template(
self.entity_id, variables, None
)
if self.device_class not in {
@@ -190,7 +199,7 @@ class CommandSensor(ManualTriggerSensorEntity):
value, self.entity_id, self.device_class
)
self._process_manual_data(value)
self._process_manual_data(variables)
self.async_write_ha_state()
async def async_update(self) -> None:
@@ -19,7 +19,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.template import Template
from homeassistant.helpers.trigger_template_entity import ManualTriggerEntity
from homeassistant.helpers.trigger_template_entity import (
ManualTriggerEntity,
ValueTemplate,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util import dt as dt_util, slugify
@@ -78,7 +81,7 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
command_on: str,
command_off: str,
command_state: str | None,
value_template: Template | None,
value_template: ValueTemplate | None,
timeout: int,
scan_interval: timedelta,
) -> None:
@@ -166,15 +169,21 @@ class CommandSwitch(ManualTriggerEntity, SwitchEntity):
"""Update device state."""
if self._command_state:
payload = str(await self._async_query_state())
variables = self._template_variables_with_value(payload)
if not self._render_availability_template(variables):
self.async_write_ha_state()
return
value = None
if self._value_template:
value = self._value_template.async_render_with_possible_json_value(
payload, None
value = self._value_template.async_render_as_value_template(
self.entity_id, variables, None
)
self._attr_is_on = None
if payload or value:
self._attr_is_on = (value or payload).lower() == "true"
self._process_manual_data(payload)
self._process_manual_data(variables)
self.async_write_ha_state()
async def async_update(self) -> None:
+1 -1
View File
@@ -73,7 +73,7 @@
"remote_moved_any_side": "Device moved with any side up",
"remote_double_tap_any_side": "Device double tapped on any side",
"remote_turned_clockwise": "Device turned clockwise",
"remote_turned_counter_clockwise": "Device turned counter clockwise",
"remote_turned_counter_clockwise": "Device turned counterclockwise",
"remote_rotate_from_side_1": "Device rotated from \"side 1\" to \"{subtype}\"",
"remote_rotate_from_side_2": "Device rotated from \"side 2\" to \"{subtype}\"",
"remote_rotate_from_side_3": "Device rotated from \"side 3\" to \"{subtype}\"",
@@ -218,7 +218,7 @@ class TrackerEntity(
entity_description: TrackerEntityDescription
_attr_latitude: float | None = None
_attr_location_accuracy: int = 0
_attr_location_accuracy: float = 0
_attr_location_name: str | None = None
_attr_longitude: float | None = None
_attr_source_type: SourceType = SourceType.GPS
@@ -234,7 +234,7 @@ class TrackerEntity(
return not self.should_poll
@cached_property
def location_accuracy(self) -> int:
def location_accuracy(self) -> float:
"""Return the location accuracy of the device.
Value in meters.
@@ -138,7 +138,7 @@ async def async_setup_entry(
SENSOR_TYPES[CONNECTED_PLC_DEVICES],
)
)
network = await device.plcnet.async_get_network_overview()
network: LogicalNetwork = coordinators[CONNECTED_PLC_DEVICES].data
peers = [
peer.mac_address for peer in network.devices if peer.topology == REMOTE
]
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Set up the Dialogflow Webhook",
"title": "Set up the Dialogflow webhook",
"description": "Are you sure you want to set up Dialogflow?"
}
},
@@ -12,7 +12,7 @@
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to set up [webhook integration of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
"default": "To send events to Home Assistant, you will need to set up the [webhook service of Dialogflow]({dialogflow_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) for further details."
}
}
}
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "bronze",
"requirements": ["pyenphase==1.25.5"],
"requirements": ["pyenphase==1.26.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.10.1"]
"requirements": ["env-canada==0.10.2"]
}
@@ -86,7 +86,7 @@
"name": "AQHI"
},
"advisories": {
"name": "Advisory"
"name": "Advisories"
},
"endings": {
"name": "Endings"
@@ -94,6 +94,7 @@ class EphEmberThermostat(ClimateEntity):
self._ember = ember
self._zone_name = zone_name(zone)
self._zone = zone
self._attr_unique_id = zone["zoneid"]
# hot water = true, is immersive device without target temperature control.
self._hot_water = zone_is_hotwater(zone)
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.13.1"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
}
@@ -42,7 +42,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .entity import EsphomeAssistEntity, convert_api_error_ha_error
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
from .entry_data import ESPHomeConfigEntry
from .enum_mapper import EsphomeEnumMapper
from .ffmpeg_proxy import async_create_proxy_url
@@ -96,7 +96,7 @@ async def async_setup_entry(
if entry_data.device_info.voice_assistant_feature_flags_compat(
entry_data.api_version
):
async_add_entities([EsphomeAssistSatellite(entry, entry_data)])
async_add_entities([EsphomeAssistSatellite(entry)])
class EsphomeAssistSatellite(
@@ -108,17 +108,12 @@ class EsphomeAssistSatellite(
key="assist_satellite", translation_key="assist_satellite"
)
def __init__(
self,
config_entry: ESPHomeConfigEntry,
entry_data: RuntimeEntryData,
) -> None:
def __init__(self, entry: ESPHomeConfigEntry) -> None:
"""Initialize satellite."""
super().__init__(entry_data)
super().__init__(entry.runtime_data)
self.config_entry = config_entry
self.entry_data = entry_data
self.cli = self.entry_data.client
self.config_entry = entry
self.cli = self._entry_data.client
self._is_running: bool = True
self._pipeline_task: asyncio.Task | None = None
@@ -134,23 +129,23 @@ class EsphomeAssistSatellite(
@property
def pipeline_entity_id(self) -> str | None:
"""Return the entity ID of the pipeline to use for the next conversation."""
assert self.entry_data.device_info is not None
assert self._entry_data.device_info is not None
ent_reg = er.async_get(self.hass)
return ent_reg.async_get_entity_id(
Platform.SELECT,
DOMAIN,
f"{self.entry_data.device_info.mac_address}-pipeline",
f"{self._entry_data.device_info.mac_address}-pipeline",
)
@property
def vad_sensitivity_entity_id(self) -> str | None:
"""Return the entity ID of the VAD sensitivity to use for the next conversation."""
assert self.entry_data.device_info is not None
assert self._entry_data.device_info is not None
ent_reg = er.async_get(self.hass)
return ent_reg.async_get_entity_id(
Platform.SELECT,
DOMAIN,
f"{self.entry_data.device_info.mac_address}-vad_sensitivity",
f"{self._entry_data.device_info.mac_address}-vad_sensitivity",
)
@callback
@@ -196,16 +191,16 @@ class EsphomeAssistSatellite(
_LOGGER.debug("Received satellite configuration: %s", self._satellite_config)
# Inform listeners that config has been updated
self.entry_data.async_assist_satellite_config_updated(self._satellite_config)
self._entry_data.async_assist_satellite_config_updated(self._satellite_config)
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added to hass."""
await super().async_added_to_hass()
assert self.entry_data.device_info is not None
assert self._entry_data.device_info is not None
feature_flags = (
self.entry_data.device_info.voice_assistant_feature_flags_compat(
self.entry_data.api_version
self._entry_data.device_info.voice_assistant_feature_flags_compat(
self._entry_data.api_version
)
)
if feature_flags & VoiceAssistantFeature.API_AUDIO:
@@ -261,7 +256,7 @@ class EsphomeAssistSatellite(
# Update wake word select when config is updated
self.async_on_remove(
self.entry_data.async_register_assist_satellite_set_wake_word_callback(
self._entry_data.async_register_assist_satellite_set_wake_word_callback(
self.async_set_wake_word
)
)
@@ -283,7 +278,7 @@ class EsphomeAssistSatellite(
data_to_send: dict[str, Any] = {}
if event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_START:
self.entry_data.async_set_assist_pipeline_state(True)
self._entry_data.async_set_assist_pipeline_state(True)
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
assert event.data is not None
data_to_send = {"text": event.data["stt_output"]["text"]}
@@ -305,10 +300,10 @@ class EsphomeAssistSatellite(
url = async_process_play_media_url(self.hass, path)
data_to_send = {"url": url}
assert self.entry_data.device_info is not None
assert self._entry_data.device_info is not None
feature_flags = (
self.entry_data.device_info.voice_assistant_feature_flags_compat(
self.entry_data.api_version
self._entry_data.device_info.voice_assistant_feature_flags_compat(
self._entry_data.api_version
)
)
if feature_flags & VoiceAssistantFeature.SPEAKER and (
@@ -344,7 +339,7 @@ class EsphomeAssistSatellite(
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_END:
if self._tts_streaming_task is None:
# No TTS
self.entry_data.async_set_assist_pipeline_state(False)
self._entry_data.async_set_assist_pipeline_state(False)
self.cli.send_voice_assistant_event(event_type, data_to_send)
@@ -386,7 +381,7 @@ class EsphomeAssistSatellite(
# Route media through the proxy
format_to_use: MediaPlayerSupportedFormat | None = None
for supported_format in chain(
*self.entry_data.media_player_formats.values()
*self._entry_data.media_player_formats.values()
):
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
format_to_use = supported_format
@@ -444,10 +439,10 @@ class EsphomeAssistSatellite(
# API or UDP output audio
port: int = 0
assert self.entry_data.device_info is not None
assert self._entry_data.device_info is not None
feature_flags = (
self.entry_data.device_info.voice_assistant_feature_flags_compat(
self.entry_data.api_version
self._entry_data.device_info.voice_assistant_feature_flags_compat(
self._entry_data.api_version
)
)
if (feature_flags & VoiceAssistantFeature.SPEAKER) and not (
@@ -548,7 +543,7 @@ class EsphomeAssistSatellite(
def _update_tts_format(self) -> None:
"""Update the TTS format from the first media player."""
for supported_format in chain(*self.entry_data.media_player_formats.values()):
for supported_format in chain(*self._entry_data.media_player_formats.values()):
# Find first announcement format
if supported_format.purpose == MediaPlayerFormatPurpose.ANNOUNCEMENT:
self._attr_tts_options = {
@@ -634,7 +629,7 @@ class EsphomeAssistSatellite(
# State change
self.tts_response_finished()
self.entry_data.async_set_assist_pipeline_state(False)
self._entry_data.async_set_assist_pipeline_state(False)
async def _wrap_audio_stream(self) -> AsyncIterable[bytes]:
"""Yield audio chunks from the queue until None."""
@@ -2,50 +2,22 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from functools import partial
from aioesphomeapi import BinarySensorInfo, BinarySensorState, EntityInfo
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.core import callback
from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN
from .entity import EsphomeAssistEntity, EsphomeEntity, platform_async_setup_entry
from .entry_data import ESPHomeConfigEntry
from .entity import EsphomeEntity, platform_async_setup_entry
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
entry: ESPHomeConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ESPHome binary sensors based on a config entry."""
await platform_async_setup_entry(
hass,
entry,
async_add_entities,
info_type=BinarySensorInfo,
entity_type=EsphomeBinarySensor,
state_type=BinarySensorState,
)
entry_data = entry.runtime_data
assert entry_data.device_info is not None
if entry_data.device_info.voice_assistant_feature_flags_compat(
entry_data.api_version
):
async_add_entities([EsphomeAssistInProgressBinarySensor(entry_data)])
class EsphomeBinarySensor(
EsphomeEntity[BinarySensorInfo, BinarySensorState], BinarySensorEntity
):
@@ -76,50 +48,9 @@ class EsphomeBinarySensor(
return self._static_info.is_status_binary_sensor or super().available
class EsphomeAssistInProgressBinarySensor(EsphomeAssistEntity, BinarySensorEntity):
"""A binary sensor implementation for ESPHome for use with assist_pipeline."""
entity_description = BinarySensorEntityDescription(
entity_registry_enabled_default=False,
key="assist_in_progress",
translation_key="assist_in_progress",
)
async def async_added_to_hass(self) -> None:
"""Create issue."""
await super().async_added_to_hass()
if TYPE_CHECKING:
assert self.registry_entry is not None
ir.async_create_issue(
self.hass,
DOMAIN,
f"assist_in_progress_deprecated_{self.registry_entry.id}",
breaks_in_ha_version="2025.4",
data={
"entity_id": self.entity_id,
"entity_uuid": self.registry_entry.id,
"integration_name": "ESPHome",
},
is_fixable=True,
severity=ir.IssueSeverity.WARNING,
translation_key="assist_in_progress_deprecated",
translation_placeholders={
"integration_name": "ESPHome",
},
)
async def async_will_remove_from_hass(self) -> None:
"""Remove issue."""
await super().async_will_remove_from_hass()
if TYPE_CHECKING:
assert self.registry_entry is not None
ir.async_delete_issue(
self.hass,
DOMAIN,
f"assist_in_progress_deprecated_{self.registry_entry.id}",
)
@property
def is_on(self) -> bool | None:
"""Return true if the binary sensor is on."""
return self._entry_data.assist_pipeline_state
async_setup_entry = partial(
platform_async_setup_entry,
info_type=BinarySensorInfo,
entity_type=EsphomeBinarySensor,
state_type=BinarySensorState,
)
@@ -177,7 +177,7 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
)
async def async_step_reconfigure(
self, entry_data: Mapping[str, Any]
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a flow initialized by a reconfig request."""
self._reconfig_entry = self._get_reconfigure_entry()
@@ -323,7 +323,9 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
):
return
assert conflict_entry.unique_id is not None
if updates:
if self.source == SOURCE_RECONFIGURE:
error = "reconfigure_already_configured"
elif updates:
error = "already_configured_updates"
else:
error = "already_configured_detailed"
@@ -5,43 +5,38 @@ from __future__ import annotations
from datetime import timedelta
import logging
import aiohttp
from awesomeversion import AwesomeVersion
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
REFRESH_INTERVAL = timedelta(minutes=5)
class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):
"""Class to interact with the ESPHome dashboard."""
def __init__(
self,
hass: HomeAssistant,
addon_slug: str,
url: str,
session: aiohttp.ClientSession,
) -> None:
"""Initialize."""
def __init__(self, hass: HomeAssistant, addon_slug: str, url: str) -> None:
"""Initialize the dashboard coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=None,
name="ESPHome Dashboard",
update_interval=timedelta(minutes=5),
update_interval=REFRESH_INTERVAL,
always_update=False,
)
self.addon_slug = addon_slug
self.url = url
self.api = ESPHomeDashboardAPI(url, session)
self.api = ESPHomeDashboardAPI(url, async_get_clientsession(hass))
self.supports_update: bool | None = None
async def _async_update_data(self) -> dict:
async def _async_update_data(self) -> dict[str, ConfiguredDevice]:
"""Fetch device data."""
devices = await self.api.get_devices()
configured_devices = devices["configured"]
@@ -9,7 +9,6 @@ from typing import Any
from homeassistant.config_entries import SOURCE_REAUTH
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.hassio import is_hassio
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store
@@ -104,9 +103,7 @@ class ESPHomeDashboardManager:
self._cancel_shutdown = None
self._current_dashboard = None
dashboard = ESPHomeDashboardCoordinator(
hass, addon_slug, url, async_get_clientsession(hass)
)
dashboard = ESPHomeDashboardCoordinator(hass, addon_slug, url)
await dashboard.async_request_refresh()
self._current_dashboard = dashboard
+1 -1
View File
@@ -109,7 +109,7 @@ def _mired_to_kelvin(mired_temperature: float) -> int:
def _color_mode_to_ha(mode: int) -> str:
"""Convert an esphome color mode to a HA color mode constant.
Chose the color mode that best matches the feature-set.
Choose the color mode that best matches the feature-set.
"""
candidates = []
for ha_mode, cap_lists in _COLOR_MODE_MAPPING.items():
@@ -49,6 +49,7 @@ from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
issue_registry as ir,
template,
)
from homeassistant.helpers.device_registry import format_mac
@@ -654,6 +655,30 @@ class ESPHomeManager:
):
self._async_subscribe_logs(new_log_level)
@callback
def _async_cleanup(self) -> None:
"""Cleanup stale issues and entities."""
assert self.entry_data.device_info is not None
ent_reg = er.async_get(self.hass)
# Cleanup stale assist_in_progress entity and issue,
# Remove this after 2026.4
if not (
stale_entry_entity_id := ent_reg.async_get_entity_id(
DOMAIN,
Platform.BINARY_SENSOR,
f"{self.entry_data.device_info.mac_address}-assist_in_progress",
)
):
return
stale_entry = ent_reg.async_get(stale_entry_entity_id)
assert stale_entry is not None
ent_reg.async_remove(stale_entry_entity_id)
issue_reg = ir.async_get(self.hass)
if issue := issue_reg.async_get_issue(
DOMAIN, f"assist_in_progress_deprecated_{stale_entry.id}"
):
issue_reg.async_delete(DOMAIN, issue.issue_id)
async def async_start(self) -> None:
"""Start the esphome connection manager."""
hass = self.hass
@@ -696,6 +721,7 @@ class ESPHomeManager:
_setup_services(hass, entry_data, services)
if (device_info := entry_data.device_info) is not None:
self._async_cleanup()
if device_info.name:
reconnect_logic.name = device_info.name
if (
@@ -15,10 +15,11 @@
"iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==30.0.1",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.13.1"
"bleak-esphome==2.14.0"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -96,7 +96,7 @@ class EsphomeMediaPlayer(
@property
@esphome_float_state_property
def volume_level(self) -> float | None:
def volume_level(self) -> float:
"""Volume level of the media player (0..1)."""
return self._state.volume
@@ -0,0 +1,85 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
Since actions are defined per device, rather than per integration,
they are specific to the device's YAML configuration. Additionally,
ESPHome allows for user-defined actions, making it impossible to
set them up until the device is connected as they vary by device. For more
information, see: https://esphome.io/components/api.html#user-defined-actions
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
Since actions are defined per device, rather than per integration,
they are specific to the device's YAML configuration. Additionally,
ESPHome allows for user-defined actions, making it difficult to provide
standard documentation since these actions vary by device. For more
information, see: https://esphome.io/components/api.html#user-defined-actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup: done
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup:
status: exempt
comment: |
ESPHome relies on sleepy devices and fast reconnect logic, so we
can't raise `ConfigEntryNotReady`. Instead, we need to utilize the
reconnect logic in `aioesphomeapi` to determine the right moment
to trigger the connection.
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples:
status: exempt
comment: |
Since ESPHome is a framework for creating custom devices, the
possibilities are virtually limitless. As a result, example
automations would likely only be relevant to the specific user
of the device and not generally useful to others.
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices: done
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done
@@ -7,9 +7,6 @@ from typing import cast
import voluptuous as vol
from homeassistant import data_entry_flow
from homeassistant.components.assist_pipeline.repair_flows import (
AssistInProgressDeprecatedRepairFlow,
)
from homeassistant.components.repairs import RepairsFlow
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import issue_registry as ir
@@ -99,8 +96,6 @@ async def async_create_fix_flow(
data: dict[str, str | int | float | None] | None,
) -> RepairsFlow:
"""Create flow."""
if issue_id.startswith("assist_in_progress_deprecated"):
return AssistInProgressDeprecatedRepairFlow(data)
if issue_id.startswith("device_conflict"):
return DeviceConflictRepair(data)
# If ESPHome adds confirm-only repairs in the future, this should be changed
+2 -3
View File
@@ -52,7 +52,7 @@ async def async_setup_entry(
[
EsphomeAssistPipelineSelect(hass, entry_data),
EsphomeVadSensitivitySelect(hass, entry_data),
EsphomeAssistSatelliteWakeWordSelect(hass, entry_data),
EsphomeAssistSatelliteWakeWordSelect(entry_data),
]
)
@@ -107,11 +107,10 @@ class EsphomeAssistSatelliteWakeWordSelect(
translation_key="wake_word",
entity_category=EntityCategory.CONFIG,
)
_attr_should_poll = False
_attr_current_option: str | None = None
_attr_options: list[str] = []
def __init__(self, hass: HomeAssistant, entry_data: RuntimeEntryData) -> None:
def __init__(self, entry_data: RuntimeEntryData) -> None:
"""Initialize a wake word selector."""
EsphomeAssistEntity.__init__(self, entry_data)
@@ -4,6 +4,7 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_configured_detailed": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`.",
"already_configured_updates": "A device `{name}`, with MAC address `{mac}` is already configured as `{title}`; the existing configuration will be updated with the validated data.",
"reconfigure_already_configured": "A device `{name}` with MAC address `{mac}` is already configured as `{title}`. Reconfiguration was aborted because the new configuration appears to refer to a different device.",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"mdns_missing_mac": "Missing MAC address in mDNS properties.",
@@ -102,11 +103,6 @@
"name": "[%key:component::assist_satellite::entity_component::_::name%]"
}
},
"binary_sensor": {
"assist_in_progress": {
"name": "[%key:component::assist_pipeline::entity::binary_sensor::assist_in_progress::name%]"
}
},
"select": {
"pipeline": {
"name": "[%key:component::assist_pipeline::entity::select::pipeline::name%]",
+1 -1
View File
@@ -36,7 +36,7 @@ class EsphomeSwitch(EsphomeEntity[SwitchInfo, SwitchState], SwitchEntity):
@property
@esphome_state_property
def is_on(self) -> bool | None:
def is_on(self) -> bool:
"""Return true if the switch is on."""
return self._state.state
+10 -10
View File
@@ -70,7 +70,6 @@ async def async_setup_entry(
@callback
def _async_setup_update_entity() -> None:
"""Set up the update entity."""
nonlocal unsubs
assert dashboard is not None
# Keep listening until device is available
if not entry_data.available or not dashboard.last_update_success:
@@ -95,10 +94,12 @@ async def async_setup_entry(
_async_setup_update_entity()
return
unsubs = [
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
dashboard.async_add_listener(_async_setup_update_entity),
]
unsubs.extend(
[
entry_data.async_subscribe_device_updated(_async_setup_update_entity),
dashboard.async_add_listener(_async_setup_update_entity),
]
)
class ESPHomeDashboardUpdateEntity(
@@ -109,7 +110,6 @@ class ESPHomeDashboardUpdateEntity(
_attr_has_entity_name = True
_attr_device_class = UpdateDeviceClass.FIRMWARE
_attr_title = "ESPHome"
_attr_name = "Firmware"
_attr_release_url = "https://esphome.io/changelog/"
_attr_entity_registry_enabled_default = False
@@ -242,7 +242,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
@property
@esphome_state_property
def installed_version(self) -> str | None:
def installed_version(self) -> str:
"""Return the installed version."""
return self._state.current_version
@@ -260,19 +260,19 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
@property
@esphome_state_property
def release_summary(self) -> str | None:
def release_summary(self) -> str:
"""Return the release summary."""
return self._state.release_summary
@property
@esphome_state_property
def release_url(self) -> str | None:
def release_url(self) -> str:
"""Return the release URL."""
return self._state.release_url
@property
@esphome_state_property
def title(self) -> str | None:
def title(self) -> str:
"""Return the title of the update."""
return self._state.title
+1 -1
View File
@@ -65,7 +65,7 @@ class EsphomeValve(EsphomeEntity[ValveInfo, ValveState], ValveEntity):
@property
@esphome_state_property
def current_valve_position(self) -> int | None:
def current_valve_position(self) -> int:
"""Return current position of valve. 0 is closed, 100 is open."""
return round(self._state.position * 100.0)
@@ -22,19 +22,14 @@ from .entity import FritzBoxDeviceEntity
from .model import FritzEntityDescriptionMixinBase
@dataclass(frozen=True)
class FritzEntityDescriptionMixinBinarySensor(FritzEntityDescriptionMixinBase):
"""BinarySensor description mixin for Fritz!Smarthome entities."""
is_on: Callable[[FritzhomeDevice], bool | None]
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class FritzBinarySensorEntityDescription(
BinarySensorEntityDescription, FritzEntityDescriptionMixinBinarySensor
BinarySensorEntityDescription, FritzEntityDescriptionMixinBase
):
"""Description for Fritz!Smarthome binary sensor entities."""
is_on: Callable[[FritzhomeDevice], bool | None]
BINARY_SENSOR_TYPES: Final[tuple[FritzBinarySensorEntityDescription, ...]] = (
FritzBinarySensorEntityDescription(
+3 -9
View File
@@ -35,20 +35,14 @@ from .entity import FritzBoxDeviceEntity
from .model import FritzEntityDescriptionMixinBase
@dataclass(frozen=True)
class FritzEntityDescriptionMixinSensor(FritzEntityDescriptionMixinBase):
"""Sensor description mixin for Fritz!Smarthome entities."""
native_value: Callable[[FritzhomeDevice], StateType | datetime]
@dataclass(frozen=True)
@dataclass(frozen=True, kw_only=True)
class FritzSensorEntityDescription(
SensorEntityDescription, FritzEntityDescriptionMixinSensor
SensorEntityDescription, FritzEntityDescriptionMixinBase
):
"""Description for Fritz!Smarthome sensor entities."""
entity_category_fn: Callable[[FritzhomeDevice], EntityCategory | None] | None = None
native_value: Callable[[FritzhomeDevice], StateType | datetime]
def suitable_eco_temperature(device: FritzhomeDevice) -> bool:
@@ -2,8 +2,8 @@
"config": {
"step": {
"user": {
"title": "Set up the Geofency Webhook",
"description": "Are you sure you want to set up the Geofency Webhook?"
"title": "Set up the Geofency webhook",
"description": "Are you sure you want to set up the Geofency webhook?"
}
},
"abort": {
@@ -16,7 +16,7 @@ RECOMMENDED_TOP_P = 0.95
CONF_TOP_K = "top_k"
RECOMMENDED_TOP_K = 64
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 150
RECOMMENDED_MAX_TOKENS = 1500
CONF_HARASSMENT_BLOCK_THRESHOLD = "harassment_block_threshold"
CONF_HATE_BLOCK_THRESHOLD = "hate_block_threshold"
CONF_SEXUAL_BLOCK_THRESHOLD = "sexual_block_threshold"
@@ -2,8 +2,8 @@
"config": {
"step": {
"user": {
"title": "Set up the GPSLogger Webhook",
"description": "Are you sure you want to set up the GPSLogger Webhook?"
"title": "Set up the GPSLogger webhook",
"description": "Are you sure you want to set up the GPSLogger webhook?"
}
},
"abort": {
@@ -104,7 +104,6 @@ from .const import (
)
from .coordinator import (
HassioDataUpdateCoordinator,
get_addons_changelogs, # noqa: F401
get_addons_info,
get_addons_stats, # noqa: F401
get_core_info, # noqa: F401
+2 -2
View File
@@ -46,13 +46,13 @@ from homeassistant.components.backup import (
RestoreBackupStage,
RestoreBackupState,
WrittenBackup,
async_get_manager as async_get_backup_manager,
suggested_filename as suggested_backup_filename,
suggested_filename_from_name_date,
)
from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.backup import async_get_manager as async_get_backup_manager
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import dt as dt_util
from homeassistant.util.enum import try_parse_enum
@@ -767,7 +767,7 @@ async def backup_addon_before_update(
async def backup_core_before_update(hass: HomeAssistant) -> None:
"""Prepare for updating core."""
backup_manager = async_get_backup_manager(hass)
backup_manager = await async_get_backup_manager(hass)
client = get_supervisor_client(hass)
try:
+1 -4
View File
@@ -85,7 +85,6 @@ DATA_OS_INFO = "hassio_os_info"
DATA_NETWORK_INFO = "hassio_network_info"
DATA_SUPERVISOR_INFO = "hassio_supervisor_info"
DATA_SUPERVISOR_STATS = "hassio_supervisor_stats"
DATA_ADDONS_CHANGELOGS = "hassio_addons_changelogs"
DATA_ADDONS_INFO = "hassio_addons_info"
DATA_ADDONS_STATS = "hassio_addons_stats"
HASSIO_UPDATE_INTERVAL = timedelta(minutes=5)
@@ -94,7 +93,6 @@ ATTR_AUTO_UPDATE = "auto_update"
ATTR_VERSION = "version"
ATTR_VERSION_LATEST = "version_latest"
ATTR_CPU_PERCENT = "cpu_percent"
ATTR_CHANGELOG = "changelog"
ATTR_LOCATION = "location"
ATTR_MEMORY_PERCENT = "memory_percent"
ATTR_SLUG = "slug"
@@ -124,14 +122,13 @@ CORE_CONTAINER = "homeassistant"
SUPERVISOR_CONTAINER = "hassio_supervisor"
CONTAINER_STATS = "stats"
CONTAINER_CHANGELOG = "changelog"
CONTAINER_INFO = "info"
# This is a mapping of which endpoint the key in the addon data
# is obtained from so we know which endpoint to update when the
# coordinator polls for updates.
KEY_TO_UPDATE_TYPES: dict[str, set[str]] = {
ATTR_VERSION_LATEST: {CONTAINER_INFO, CONTAINER_CHANGELOG},
ATTR_VERSION_LATEST: {CONTAINER_INFO},
ATTR_MEMORY_PERCENT: {CONTAINER_STATS},
ATTR_CPU_PERCENT: {CONTAINER_STATS},
ATTR_VERSION: {CONTAINER_INFO},
+7 -36
View File
@@ -7,7 +7,7 @@ from collections import defaultdict
import logging
from typing import TYPE_CHECKING, Any
from aiohasupervisor import SupervisorError
from aiohasupervisor import SupervisorError, SupervisorNotFoundError
from aiohasupervisor.models import StoreInfo
from homeassistant.config_entries import ConfigEntry
@@ -21,18 +21,15 @@ from homeassistant.loader import bind_hass
from .const import (
ATTR_AUTO_UPDATE,
ATTR_CHANGELOG,
ATTR_REPOSITORY,
ATTR_SLUG,
ATTR_STARTED,
ATTR_STATE,
ATTR_URL,
ATTR_VERSION,
CONTAINER_CHANGELOG,
CONTAINER_INFO,
CONTAINER_STATS,
CORE_CONTAINER,
DATA_ADDONS_CHANGELOGS,
DATA_ADDONS_INFO,
DATA_ADDONS_STATS,
DATA_COMPONENT,
@@ -155,16 +152,6 @@ def get_supervisor_stats(hass: HomeAssistant) -> dict[str, Any]:
return hass.data.get(DATA_SUPERVISOR_STATS) or {}
@callback
@bind_hass
def get_addons_changelogs(hass: HomeAssistant):
"""Return Addons changelogs.
Async friendly.
"""
return hass.data.get(DATA_ADDONS_CHANGELOGS)
@callback
@bind_hass
def get_os_info(hass: HomeAssistant) -> dict[str, Any] | None:
@@ -337,7 +324,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
supervisor_info = get_supervisor_info(self.hass) or {}
addons_info = get_addons_info(self.hass) or {}
addons_stats = get_addons_stats(self.hass)
addons_changelogs = get_addons_changelogs(self.hass)
store_data = get_store(self.hass)
if store_data:
@@ -355,7 +341,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
ATTR_AUTO_UPDATE: (addons_info.get(addon[ATTR_SLUG]) or {}).get(
ATTR_AUTO_UPDATE, False
),
ATTR_CHANGELOG: (addons_changelogs or {}).get(addon[ATTR_SLUG]),
ATTR_REPOSITORY: repositories.get(
addon.get(ATTR_REPOSITORY), addon.get(ATTR_REPOSITORY, "")
),
@@ -422,10 +407,12 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
return new_data
async def force_info_update_supervisor(self) -> None:
"""Force update of the supervisor info."""
self.hass.data[DATA_SUPERVISOR_INFO] = await self.hassio.get_supervisor_info()
await self.async_refresh()
async def get_changelog(self, addon_slug: str) -> str | None:
"""Get the changelog for an add-on."""
try:
return await self.supervisor_client.store.addon_changelog(addon_slug)
except SupervisorNotFoundError:
return None
async def force_data_refresh(self, first_update: bool) -> None:
"""Force update of the addon info."""
@@ -475,13 +462,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
started_addons,
False,
),
(
DATA_ADDONS_CHANGELOGS,
self._update_addon_changelog,
CONTAINER_CHANGELOG,
all_addons,
True,
),
(
DATA_ADDONS_INFO,
self._update_addon_info,
@@ -513,15 +493,6 @@ class HassioDataUpdateCoordinator(DataUpdateCoordinator):
return (slug, None)
return (slug, stats.to_dict())
async def _update_addon_changelog(self, slug: str) -> tuple[str, str | None]:
"""Return the changelog for an add-on."""
try:
changelog = await self.supervisor_client.store.addon_changelog(slug)
except SupervisorError as err:
_LOGGER.warning("Could not fetch changelog for %s: %s", slug, err)
return (slug, None)
return (slug, changelog)
async def _update_addon_info(self, slug: str) -> tuple[str, dict[str, Any] | None]:
"""Return the info for an add-on."""
try:
@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/hassio",
"iot_class": "local_polling",
"quality_scale": "internal",
"requirements": ["aiohasupervisor==0.3.1b1"],
"requirements": ["aiohasupervisor==0.3.1"],
"single_config_entry": true
}
+16 -26
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import re
from typing import Any
from aiohasupervisor import SupervisorError
@@ -21,7 +22,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import (
ADDONS_COORDINATOR,
ATTR_AUTO_UPDATE,
ATTR_CHANGELOG,
ATTR_VERSION,
ATTR_VERSION_LATEST,
DATA_KEY_ADDONS,
@@ -116,11 +116,6 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
"""Version installed and in use."""
return self._addon_data[ATTR_VERSION]
@property
def release_summary(self) -> str | None:
"""Release summary for the add-on."""
return self._strip_release_notes()
@property
def entity_picture(self) -> str | None:
"""Return the icon of the add-on if any."""
@@ -130,27 +125,22 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
return f"/api/hassio/addons/{self._addon_slug}/icon"
return None
def _strip_release_notes(self) -> str | None:
"""Strip the release notes to contain the needed sections."""
if (notes := self._addon_data[ATTR_CHANGELOG]) is None:
return None
if (
f"# {self.latest_version}" in notes
and f"# {self.installed_version}" in notes
):
# Split the release notes to only what is between the versions if we can
new_notes = notes.split(f"# {self.installed_version}")[0]
if f"# {self.latest_version}" in new_notes:
# Make sure the latest version is still there.
# This can be False if the order of the release notes are not correct
# In that case we just return the whole release notes
return new_notes
return notes
async def async_release_notes(self) -> str | None:
"""Return the release notes for the update."""
return self._strip_release_notes()
if (
changelog := await self.coordinator.get_changelog(self._addon_slug)
) is None:
return None
if self.latest_version is None or self.installed_version is None:
return changelog
regex_pattern = re.compile(
rf"^#* {re.escape(self.latest_version)}\n(?:^(?!#* {re.escape(self.installed_version)}).*\n)*",
re.MULTILINE,
)
match = regex_pattern.search(changelog)
return match.group(0) if match else changelog
async def async_install(
self,
@@ -162,7 +152,7 @@ class SupervisorAddonUpdateEntity(HassioAddonEntity, UpdateEntity):
await update_addon(
self.hass, self._addon_slug, backup, self.title, self.installed_version
)
await self.coordinator.force_info_update_supervisor()
await self.coordinator.async_refresh()
class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.0"]
"requirements": ["homematicip==2.0.1"]
}
@@ -9,6 +9,7 @@ from huawei_lte_api.enums.cradle import ConnectionStatusEnum
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
@@ -104,6 +105,7 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor):
_attr_translation_key = "mobile_connection"
_attr_entity_registry_enabled_default = True
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
key = KEY_MONITORING_STATUS
item = "ConnectionStatus"
@@ -140,6 +142,8 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor):
class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor):
"""Huawei LTE WiFi status binary sensor base class."""
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
@property
def is_on(self) -> bool:
"""Return whether the binary sensor is on."""
@@ -543,6 +543,7 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
descriptions={
"BatteryPercent": HuaweiSensorEntityDescription(
key="BatteryPercent",
translation_key="battery",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -141,6 +141,9 @@
"lte_uplink_frequency": {
"name": "LTE uplink frequency"
},
"mode": {
"name": "Mode"
},
"nrbler": {
"name": "5G block error rate"
},
@@ -240,6 +243,9 @@
"current_month_upload": {
"name": "Current month upload"
},
"battery": {
"name": "Battery"
},
"wifi_clients_connected": {
"name": "Wi-Fi clients connected"
},
@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2025.4.0"]
"requirements": ["aioautomower==2025.4.4"]
}
@@ -44,8 +44,8 @@ async def async_set_work_area_cutting_height(
) -> None:
"""Set cutting height for work area."""
await coordinator.api.commands.workarea_settings(
mower_id, work_area_id, cutting_height=int(cheight)
)
mower_id, work_area_id
).cutting_height(cutting_height=int(cheight))
async def async_set_cutting_height(
@@ -206,12 +206,12 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.coordinator.api.commands.workarea_settings(
self.mower_id, self.work_area_id, enabled=False
)
self.mower_id, self.work_area_id
).enabled(enabled=False)
@handle_sending_exception(poll_after_sending=True)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.coordinator.api.commands.workarea_settings(
self.mower_id, self.work_area_id, enabled=True
)
self.mower_id, self.work_area_id
).enabled(enabled=True)
+3 -3
View File
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Set up the IFTTT Webhook Applet",
"title": "Set up the IFTTT webhook applet",
"description": "Are you sure you want to set up IFTTT?"
}
},
@@ -12,7 +12,7 @@
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT Webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
"default": "To send events to Home Assistant, you will need to use the \"Make a web request\" action from the [IFTTT webhook applet]({applet_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
}
},
"services": {
@@ -32,7 +32,7 @@
},
"trigger": {
"name": "Trigger",
"description": "Triggers the configured IFTTT Webhook.",
"description": "Triggers the configured IFTTT webhook.",
"fields": {
"event": {
"name": "Event",
@@ -16,7 +16,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"cannot_connect": "Failed to connect"
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"entity": {
@@ -21,7 +21,7 @@
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "Unexpected error"
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
+2 -14
View File
@@ -7,10 +7,8 @@ from typing import TYPE_CHECKING
from pynecil import IronOSUpdate, Pynecil
from homeassistant.components import bluetooth
from homeassistant.const import CONF_NAME, Platform
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
@@ -35,7 +33,6 @@ PLATFORMS: list[Platform] = [
Platform.UPDATE,
]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -60,17 +57,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IronOSConfigEntry) -> bo
"""Set up IronOS from a config entry."""
if TYPE_CHECKING:
assert entry.unique_id
ble_device = bluetooth.async_ble_device_from_address(
hass, entry.unique_id, connectable=True
)
if not ble_device:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_device_unavailable_exception",
translation_placeholders={CONF_NAME: entry.title},
)
device = Pynecil(ble_device)
device = Pynecil(entry.unique_id)
live_data = IronOSLiveDataCoordinator(hass, entry, device)
await live_data.async_config_entry_first_refresh()
@@ -2,9 +2,12 @@
from __future__ import annotations
import logging
from typing import Any
from bleak.exc import BleakError
from habluetooth import BluetoothServiceInfoBleak
from pynecil import CommunicationError, Pynecil
import voluptuous as vol
from homeassistant.components.bluetooth.api import async_discovered_service_info
@@ -13,6 +16,8 @@ from homeassistant.const import CONF_ADDRESS
from .const import DISCOVERY_SVC_UUID, DOMAIN
_LOGGER = logging.getLogger(__name__)
class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for IronOS."""
@@ -36,30 +41,62 @@ class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm discovery."""
errors: dict[str, str] = {}
assert self._discovery_info is not None
discovery_info = self._discovery_info
title = discovery_info.name
if user_input is not None:
return self.async_create_entry(title=title, data={})
device = Pynecil(discovery_info.address)
try:
await device.connect()
except (CommunicationError, BleakError, TimeoutError):
_LOGGER.debug("Cannot connect:", exc_info=True)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception:")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=title, data={})
finally:
await device.disconnect()
self._set_confirm_only()
placeholders = {"name": title}
self.context["title_placeholders"] = placeholders
return self.async_show_form(
step_id="bluetooth_confirm", description_placeholders=placeholders
step_id="bluetooth_confirm",
description_placeholders=placeholders,
errors=errors,
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the user step to pick discovered device."""
errors: dict[str, str] = {}
if user_input is not None:
address = user_input[CONF_ADDRESS]
title = self._discovered_devices[address]
await self.async_set_unique_id(address, raise_on_progress=False)
self._abort_if_unique_id_configured()
return self.async_create_entry(title=title, data={})
device = Pynecil(address)
try:
await device.connect()
except (CommunicationError, BleakError, TimeoutError):
_LOGGER.debug("Cannot connect:", exc_info=True)
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(title=title, data={})
finally:
await device.disconnect()
current_addresses = self._async_current_ids(include_ignore=False)
for discovery_info in async_discovered_service_info(self.hass, True):
@@ -80,4 +117,5 @@ class IronOSConfigFlow(ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema(
{vol.Required(CONF_ADDRESS): vol.In(self._discovered_devices)}
),
errors=errors,
)
+40 -19
View File
@@ -6,7 +6,7 @@ from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
import logging
from typing import cast
from typing import TYPE_CHECKING, cast
from awesomeversion import AwesomeVersion
from pynecil import (
@@ -22,10 +22,11 @@ from pynecil import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.debounce import Debouncer
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -83,14 +84,13 @@ class IronOSBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
try:
self.device_info = await self.device.get_device_info()
except CommunicationError as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={CONF_NAME: self.config_entry.title},
) from e
except (CommunicationError, TimeoutError):
self.device_info = DeviceInfoResponse()
self.v223_features = AwesomeVersion(self.device_info.build) >= V223
self.v223_features = (
self.device_info.build is not None
and AwesomeVersion(self.device_info.build) >= V223
)
class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
@@ -101,23 +101,18 @@ class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
) -> None:
"""Initialize IronOS coordinator."""
super().__init__(hass, config_entry, device, SCAN_INTERVAL)
self.device_info = DeviceInfoResponse()
async def _async_update_data(self) -> LiveDataResponse:
"""Fetch data from Device."""
try:
# device info is cached and won't be refetched on every
# coordinator refresh, only after the device has disconnected
# the device info is refetched
self.device_info = await self.device.get_device_info()
await self._update_device_info()
return await self.device.get_live_data()
except CommunicationError as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
translation_placeholders={CONF_NAME: self.config_entry.title},
) from e
except CommunicationError:
_LOGGER.debug("Cannot connect to device", exc_info=True)
return self.data or LiveDataResponse()
@property
def has_tip(self) -> bool:
@@ -130,6 +125,32 @@ class IronOSLiveDataCoordinator(IronOSBaseCoordinator[LiveDataResponse]):
return self.data.live_temp <= threshold
return False
async def _update_device_info(self) -> None:
"""Update device info.
device info is cached and won't be refetched on every
coordinator refresh, only after the device has disconnected
the device info is refetched.
"""
build = self.device_info.build
self.device_info = await self.device.get_device_info()
if build == self.device_info.build:
return
device_registry = dr.async_get(self.hass)
if TYPE_CHECKING:
assert self.config_entry.unique_id
device = device_registry.async_get_device(
connections={(CONNECTION_BLUETOOTH, self.config_entry.unique_id)}
)
if device is None:
return
device_registry.async_update_device(
device_id=device.id,
sw_version=self.device_info.build,
serial_number=f"{self.device_info.device_sn} (ID:{self.device_info.device_id})",
)
class IronOSSettingsCoordinator(IronOSBaseCoordinator[SettingsDataResponse]):
"""IronOS coordinator."""
+12 -2
View File
@@ -37,6 +37,16 @@ class IronOSBaseEntity(CoordinatorEntity[IronOSLiveDataCoordinator]):
manufacturer=MANUFACTURER,
model=MODEL,
name="Pinecil",
sw_version=coordinator.device_info.build,
serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})",
)
if coordinator.device_info.is_synced:
self._attr_device_info.update(
DeviceInfo(
sw_version=coordinator.device_info.build,
serial_number=f"{coordinator.device_info.device_sn} (ID:{coordinator.device_info.device_id})",
)
)
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self.coordinator.device.is_connected
@@ -21,10 +21,10 @@ rules:
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure:
test-before-configure: done
test-before-setup:
status: exempt
comment: Device is set up from a Bluetooth discovery
test-before-setup: done
comment: Device is expected to be disconnected most of the time but will connect quickly when reachable
unique-config-entry: done
# Silver
@@ -47,8 +47,8 @@ rules:
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: Device is not connected to an ip network. Other information from discovery is immutable and does not require updating.
status: done
comment: Device is not connected to an ip network. FW version in device info is updated.
discovery: done
docs-data-update: done
docs-examples: done
@@ -20,7 +20,13 @@
},
"abort": {
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"entity": {
@@ -276,12 +282,6 @@
}
},
"exceptions": {
"setup_device_unavailable_exception": {
"message": "Device {name} is not reachable"
},
"setup_device_connection_error_exception": {
"message": "Connection to device {name} failed, try again later"
},
"submit_setting_failed": {
"message": "Failed to submit setting to device, try again later"
},
+7 -2
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
from homeassistant.components.update import (
ATTR_INSTALLED_VERSION,
UpdateDeviceClass,
UpdateEntity,
UpdateEntityDescription,
@@ -10,6 +11,7 @@ from homeassistant.components.update import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import IRON_OS_KEY, IronOSConfigEntry, IronOSLiveDataCoordinator
from .coordinator import IronOSFirmwareUpdateCoordinator
@@ -37,7 +39,7 @@ async def async_setup_entry(
)
class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
class IronOSUpdate(IronOSBaseEntity, UpdateEntity, RestoreEntity):
"""Representation of an IronOS update entity."""
_attr_supported_features = UpdateEntityFeature.RELEASE_NOTES
@@ -56,7 +58,7 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
def installed_version(self) -> str | None:
"""IronOS version on the device."""
return self.coordinator.device_info.build
return self.coordinator.device_info.build or self._attr_installed_version
@property
def title(self) -> str | None:
@@ -86,6 +88,9 @@ class IronOSUpdate(IronOSBaseEntity, UpdateEntity):
Register extra update listener for the firmware update coordinator.
"""
if state := await self.async_get_last_state():
self._attr_installed_version = state.attributes.get(ATTR_INSTALLED_VERSION)
await super().async_added_to_hass()
self.async_on_remove(
self.firmware_update.async_add_listener(self._handle_coordinator_update)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/ld2410_ble",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["bluetooth-data-tools==1.27.0", "ld2410-ble==0.1.1"]
"requirements": ["bluetooth-data-tools==1.28.0", "ld2410-ble==0.1.1"]
}
@@ -35,5 +35,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/led_ble",
"iot_class": "local_polling",
"requirements": ["bluetooth-data-tools==1.27.0", "led-ble==1.1.7"]
"requirements": ["bluetooth-data-tools==1.28.0", "led-ble==1.1.7"]
}
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Set up the Locative Webhook",
"title": "Set up the Locative webhook",
"description": "[%key:common::config_flow::description::confirm_setup%]"
}
},
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Set up the Mailgun Webhook",
"title": "Set up the Mailgun webhook",
"description": "Are you sure you want to set up Mailgun?"
}
},
@@ -12,7 +12,7 @@
"webhook_not_internet_accessible": "[%key:common::config_flow::abort::webhook_not_internet_accessible%]"
},
"create_entry": {
"default": "To send events to Home Assistant, you will need to set up [Webhooks with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
"default": "To send events to Home Assistant, you will need to set up a [webhook with Mailgun]({mailgun_url}).\n\nFill in the following info:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json\n\nSee [the documentation]({docs_url}) on how to configure automations to handle incoming data."
}
}
}
@@ -322,4 +322,16 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.EnergyEvse.Attributes.SupplyState,),
allow_multi=True, # also used for sensor entity
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="WaterHeaterManagementBoostStateSensor",
translation_key="boost_state",
measurement_to_ha=lambda x: (
x == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive
),
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.WaterHeaterManagement.Attributes.BoostState,),
),
]
@@ -27,6 +27,7 @@ from .switch import DISCOVERY_SCHEMAS as SWITCH_SCHEMAS
from .update import DISCOVERY_SCHEMAS as UPDATE_SCHEMAS
from .vacuum import DISCOVERY_SCHEMAS as VACUUM_SCHEMAS
from .valve import DISCOVERY_SCHEMAS as VALVE_SCHEMAS
from .water_heater import DISCOVERY_SCHEMAS as WATER_HEATER_SCHEMAS
DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
@@ -44,6 +45,7 @@ DISCOVERY_SCHEMAS: dict[Platform, list[MatterDiscoverySchema]] = {
Platform.UPDATE: UPDATE_SCHEMAS,
Platform.VACUUM: VACUUM_SCHEMAS,
Platform.VALVE: VALVE_SCHEMAS,
Platform.WATER_HEATER: WATER_HEATER_SCHEMAS,
}
SUPPORTED_PLATFORMS = tuple(DISCOVERY_SCHEMAS)
@@ -66,6 +66,12 @@
"operational_state": {
"default": "mdi:play-pause"
},
"tank_volume": {
"default": "mdi:water-boiler"
},
"tank_percentage": {
"default": "mdi:water-boiler"
},
"valve_position": {
"default": "mdi:valve"
},
@@ -41,6 +41,7 @@ type SelectCluster = (
| clusters.DishwasherMode
| clusters.EnergyEvseMode
| clusters.DeviceEnergyManagementMode
| clusters.WaterHeaterMode
)
+47 -1
View File
@@ -37,6 +37,7 @@ from homeassistant.const import (
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import HomeAssistant, callback
@@ -65,7 +66,6 @@ CONTAMINATION_STATE_MAP = {
clusters.SmokeCoAlarm.Enums.ContaminationStateEnum.kCritical: "critical",
}
OPERATIONAL_STATE_MAP = {
# enum with known Operation state values which we can translate
clusters.OperationalState.Enums.OperationalStateEnum.kStopped: "stopped",
@@ -77,6 +77,12 @@ OPERATIONAL_STATE_MAP = {
clusters.RvcOperationalState.Enums.OperationalStateEnum.kDocked: "docked",
}
BOOST_STATE_MAP = {
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive",
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active",
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kUnknownEnumValue: None,
}
EVSE_FAULT_STATE_MAP = {
clusters.EnergyEvse.Enums.FaultStateEnum.kNoError: "no_error",
clusters.EnergyEvse.Enums.FaultStateEnum.kMeterFailure: "meter_failure",
@@ -996,4 +1002,44 @@ DISCOVERY_SCHEMAS = [
entity_class=MatterSensor,
required_attributes=(clusters.EnergyEvse.Attributes.UserMaximumChargeCurrent,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="WaterHeaterManagementTankVolume",
translation_key="tank_volume",
device_class=SensorDeviceClass.VOLUME_STORAGE,
native_unit_of_measurement=UnitOfVolume.LITERS,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.WaterHeaterManagement.Attributes.TankVolume,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="WaterHeaterManagementTankPercentage",
translation_key="tank_percentage",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.WaterHeaterManagement.Attributes.TankPercentage,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="WaterHeaterManagementEstimatedHeatRequired",
translation_key="estimated_heat_required",
device_class=SensorDeviceClass.ENERGY,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfEnergy.MILLIWATT_HOUR,
suggested_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
suggested_display_precision=3,
state_class=SensorStateClass.TOTAL,
),
entity_class=MatterSensor,
required_attributes=(
clusters.WaterHeaterManagement.Attributes.EstimatedHeatRequired,
),
),
]
@@ -85,6 +85,9 @@
},
"evse_supply_charging_state": {
"name": "Supply charging state"
},
"boost_state": {
"name": "Boost state"
}
},
"button": {
@@ -229,6 +232,9 @@
},
"laundry_washer_spin_speed": {
"name": "Spin speed"
},
"water_heater_mode": {
"name": "Water heater mode"
}
},
"sensor": {
@@ -279,6 +285,15 @@
"switch_current_position": {
"name": "Current switch position"
},
"estimated_heat_required": {
"name": "Required heating energy"
},
"tank_volume": {
"name": "Tank volume"
},
"tank_percentage": {
"name": "Hot water level"
},
"valve_position": {
"name": "Valve position"
},
@@ -348,6 +363,11 @@
"valve": {
"name": "[%key:component::valve::title%]"
}
},
"water_heater": {
"water_heater": {
"name": "[%key:component::water_heater::title%]"
}
}
},
"issues": {
@@ -0,0 +1,189 @@
"""Matter water heater platform."""
from __future__ import annotations
from typing import Any, cast
from chip.clusters import Objects as clusters
from matter_server.client.models import device_types
from matter_server.common.helpers.util import create_attribute_path_from_attribute
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_HIGH_DEMAND,
STATE_OFF,
WaterHeaterEntity,
WaterHeaterEntityDescription,
WaterHeaterEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_TEMPERATURE,
PRECISION_WHOLE,
Platform,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import MatterEntity
from .helpers import get_matter
from .models import MatterDiscoverySchema
TEMPERATURE_SCALING_FACTOR = 100
# Map HA WH system mode to Matter ThermostatRunningMode attribute of the Thermostat cluster (Heat = 4)
WATER_HEATER_SYSTEM_MODE_MAP = {
STATE_ECO: 4,
STATE_HIGH_DEMAND: 4,
STATE_OFF: 0,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Matter WaterHeater platform from Config Entry."""
matter = get_matter(hass)
matter.register_platform_handler(Platform.WATER_HEATER, async_add_entities)
class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
"""Representation of a Matter WaterHeater entity."""
_attr_current_temperature: float | None = None
_attr_current_operation: str
_attr_operation_list = [
STATE_ECO,
STATE_HIGH_DEMAND,
STATE_OFF,
]
_attr_precision = PRECISION_WHOLE
_attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.ON_OFF
| WaterHeaterEntityFeature.OPERATION_MODE
)
_attr_target_temperature: float | None = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_platform_translation_key = "water_heater"
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
target_temperature: float | None = kwargs.get(ATTR_TEMPERATURE)
if (
target_temperature is not None
and self.target_temperature != target_temperature
):
matter_attribute = clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
await self.write_attribute(
value=round(target_temperature * TEMPERATURE_SCALING_FACTOR),
matter_attribute=matter_attribute,
)
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode."""
self._attr_current_operation = operation_mode
# Boost 1h (3600s)
boost_info: type[
clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct
] = clusters.WaterHeaterManagement.Structs.WaterHeaterBoostInfoStruct(
duration=3600
)
system_mode_value = WATER_HEATER_SYSTEM_MODE_MAP[operation_mode]
await self.write_attribute(
value=system_mode_value,
matter_attribute=clusters.Thermostat.Attributes.SystemMode,
)
system_mode_path = create_attribute_path_from_attribute(
endpoint_id=self._endpoint.endpoint_id,
attribute=clusters.Thermostat.Attributes.SystemMode,
)
self._endpoint.set_attribute_value(system_mode_path, system_mode_value)
self._update_from_device()
# Trigger Boost command
if operation_mode == STATE_HIGH_DEMAND:
await self.send_device_command(
clusters.WaterHeaterManagement.Commands.Boost(boostInfo=boost_info)
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on water heater."""
await self.async_set_operation_mode("eco")
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off water heater."""
await self.async_set_operation_mode("off")
@callback
def _update_from_device(self) -> None:
"""Update from device."""
self._attr_current_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.LocalTemperature
)
self._attr_target_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
)
boost_state = self.get_matter_attribute_value(
clusters.WaterHeaterManagement.Attributes.BoostState
)
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
self._attr_current_operation = STATE_HIGH_DEMAND
else:
self._attr_current_operation = STATE_ECO
self._attr_temperature = cast(
float,
self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
),
)
self._attr_min_temp = cast(
float,
self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit
),
)
self._attr_max_temp = cast(
float,
self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit
),
)
@callback
def _get_temperature_in_degrees(
self, attribute: type[clusters.ClusterAttributeDescriptor]
) -> float | None:
"""Return the scaled temperature value for the given attribute."""
if (value := self.get_matter_attribute_value(attribute)) is not None:
return float(value) / TEMPERATURE_SCALING_FACTOR
return None
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.WATER_HEATER,
entity_description=WaterHeaterEntityDescription(
key="MatterWaterHeater",
name=None,
),
entity_class=MatterWaterHeater,
required_attributes=(
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint,
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.Attributes.LocalTemperature,
clusters.WaterHeaterManagement.Attributes.FeatureMap,
),
optional_attributes=(
clusters.WaterHeaterManagement.Attributes.HeaterTypes,
clusters.WaterHeaterManagement.Attributes.BoostState,
clusters.WaterHeaterManagement.Attributes.HeatDemand,
),
device_type=(device_types.WaterHeater,),
allow_multi=True, # also used for sensor entity
),
]

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