Compare commits

..

102 Commits

Author SHA1 Message Date
Franck Nijhof af08e5e7d0 Bump version to 2026.6.0b2 2026-06-01 21:05:58 +00:00
Franck Nijhof b03d87dc21 Cancel iCloud polling timer on config entry unload (#172793) 2026-06-01 21:05:46 +00:00
Tom d8a9ea1d9d Fix ProxmoxVE missing unused token data (#172782) 2026-06-01 21:05:44 +00:00
J. Nick Koston 5ff07fcc49 Explain why a Snooz device could not be found (#172780) 2026-06-01 21:05:42 +00:00
J. Nick Koston 6f59bb0661 Explain why an LD2410 BLE device could not be found (#172779) 2026-06-01 21:05:40 +00:00
J. Nick Koston c82d32bbae Explain why a Husqvarna Automower BLE device could not be connected to (#172774) 2026-06-01 21:05:38 +00:00
Ingo Fischer 4fbc363965 Filter stale replayed BLE advertisements in Matter BLE proxy (#172773)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 21:05:36 +00:00
J. Nick Koston 8622f0f4de Explain why an eQ-3 Bluetooth device could not be found (#172770) 2026-06-01 21:05:34 +00:00
J. Nick Koston b49a6b89b6 Bump habluetooth to 6.8.1 (#172768) 2026-06-01 21:05:32 +00:00
J. Nick Koston 0bfd4c44bb Explain why a LED BLE device could not be found (#172764) 2026-06-01 21:05:30 +00:00
J. Nick Koston c09216650f Explain why an INKBIRD device could not be found (#172762) 2026-06-01 21:05:28 +00:00
J. Nick Koston 6057d32636 Explain why a Yale Access Bluetooth device could not be found (#172761) 2026-06-01 21:05:26 +00:00
Bram Kragten 51c9d0c6e5 Bump frontend to 20260527.2 (#172759)
Co-authored-by: Franck Nijhof <git@frenck.dev>
2026-06-01 21:05:24 +00:00
J. Nick Koston 323304664e Explain why an Airthings BLE device could not be found (#172758) 2026-06-01 21:05:22 +00:00
A. Gideonse 3dda7d9848 Fix binary sensor defaults for Indevolt (#172714) 2026-06-01 21:05:20 +00:00
A. Gideonse 5e56d74257 Bump indevolt-api to 1.8.3 (#172683) 2026-06-01 21:05:18 +00:00
Thijs W. e5f9c7892a Fix get_play_status function call in frontier silicon (#172705) 2026-06-01 21:01:29 +00:00
Michael a0d713a4a7 Use proper user-agent to fetch feeds (#172655) 2026-06-01 21:01:27 +00:00
jameson_uk 84f4f876b1 media_player platform fixes for Alexa Devices (#172611) 2026-06-01 21:01:25 +00:00
Franck Nijhof 7b06228a5a Bump version to 2026.6.0b1 2026-06-01 16:54:56 +00:00
Paul Bottein 06b2ec22f0 Bump yoto-api to 3.1.5 (#172753) 2026-06-01 16:54:33 +00:00
jameson_uk 7950998083 Bump aioamazondevices to 13.8.2 (#172748) 2026-06-01 16:54:30 +00:00
Maciej Bieniek 86999063d7 Translate the name of the Tractive tracker (#172747) 2026-06-01 16:54:28 +00:00
Maciej Bieniek 9843fdad2c Add missing _attr_name = None for Tractive device tracker (#172746) 2026-06-01 16:54:26 +00:00
Jan Bouwhuis e53914a0ef Fix MQTT device_tracker logging attributes order (#172732) 2026-06-01 16:54:24 +00:00
Franck Nijhof f7afe22318 Skip Overkiz events for unknown device URLs (#172712) 2026-06-01 16:54:22 +00:00
Franck Nijhof acfecd7f5c Convert set_id to int in LG TV RS-232 config flow (#172701) 2026-06-01 16:54:20 +00:00
Franck Nijhof 56057a11e6 Return 404 instead of 500 when media player artwork is unavailable (#172700) 2026-06-01 16:54:18 +00:00
Yardian Support 0d079c57e4 Fix Yardian water hammer diagnostic sensor name (#172698) 2026-06-01 16:54:16 +00:00
Denis Shulyaka 3ad3e1fafb Fix ai_task camera snapshot mime type (#172682) 2026-06-01 16:54:13 +00:00
Josef Zweck 0677ed824f Fix tedee entity availability (#172667)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:54:11 +00:00
Jordan Harvey 4b9945e012 Bump pynintendoparental to 2.4.0 (#172666) 2026-06-01 16:54:08 +00:00
Michael 9fa0132b1c Add missing exception translation keys in Ecovacs (#172658) 2026-06-01 16:54:06 +00:00
jameson_uk 10a25368a0 Improve http2 task handling for Alexa Devices (#172649) 2026-06-01 16:54:04 +00:00
epenet fbb68c26b6 Bump tuya-device-handlers to 0.0.22 (#172648) 2026-06-01 16:54:02 +00:00
Michael 25875de414 Add extra device info to FRITZ!Box Tools diagnostics (#172647) 2026-06-01 16:54:00 +00:00
TheJulianJES 22ace88b2c Bump ZHA to 1.4.1 (#172640) 2026-06-01 16:53:57 +00:00
David Knowles a47105d314 Schlage: use lock connected status as availability signal (#172638)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:53:55 +00:00
Jan Bouwhuis b50bfda00c Fix MQTT device_tracker not saving state on location accuracy changes (#172629) 2026-06-01 16:53:53 +00:00
Sören 0d37319ba9 Improve Avea Bluetooth discovery flow (#172623) 2026-06-01 16:53:51 +00:00
Michael 24a5c75cf2 Show error about missing api permissions while browsing Immich media (#172609) 2026-06-01 16:53:49 +00:00
renovate[bot] dd43b1135d Update rf-protocols to 4.0.1 (#172597) 2026-06-01 16:53:47 +00:00
J. Nick Koston de0a202c4e Explain why a Switchbot device could not be found (#172581) 2026-06-01 16:53:44 +00:00
J. Nick Koston d550d1da90 Expose bluetooth address reachability diagnostics API (#172578) 2026-06-01 16:53:42 +00:00
J. Nick Koston ce8875ae8c Bump habluetooth to 6.8.0 (#172577) 2026-06-01 16:53:40 +00:00
J. Nick Koston 3364096b2b Fix ESPHome update entity stuck on for project versions with build suffix (#172571) 2026-06-01 16:53:38 +00:00
A. Gideonse c2b75b9634 Bugfix: Gen-1 Inverter sensor for Indevolt to display "N/A" when turned off (#172559) 2026-06-01 16:53:36 +00:00
Franck Nijhof ae278d3c80 Sanitize surrogate characters in MeteoAlarm alert attributes (#172545) 2026-06-01 16:53:34 +00:00
Paul Bottein 25f9cd9ab8 Fix Yoto OAuth flow with cloud credentials (#172544)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-06-01 16:53:31 +00:00
Franck Nijhof 796d82d6ed Add missing ssdp dependency to BraviaTV manifest (#172536) 2026-06-01 16:53:30 +00:00
Franck Nijhof 4b517fb164 Use state-based icon for Hue grouped light (#172535) 2026-06-01 16:53:27 +00:00
Kamil Breguła 2d74091a36 Refresh WLED firmware releases on manual entity update (#172517)
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-01 16:53:25 +00:00
Franck Nijhof 504e22ee3e Raise errors instead of swallowing exceptions in Toon action handlers (#172511) 2026-06-01 16:53:23 +00:00
Franck Nijhof c95a39c26e Guard Shelly repairs checks for uninitialized RPC devices (#172509) 2026-06-01 16:53:21 +00:00
Franck Nijhof 8ec3eac705 Fix Overkiz UnoIO cover reporting wrong movement direction (#172506) 2026-06-01 16:53:19 +00:00
Franck Nijhof 589d2637c9 Fix ephember crash when zone mode is None (#172504) 2026-06-01 16:53:17 +00:00
Franck Nijhof 26cf728165 Handle missing notAfter field in cert_expiry certificate data (#172503)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-01 16:51:22 +00:00
Franck Nijhof b61559bdbb Handle malformed response errors in Denon AVR error wrapper (#172502) 2026-06-01 16:06:02 +00:00
Jan Bouwhuis 57259132d9 Silent migrate MQTT protocol version to version 5 if the broker supports it or raise an issue (#172500)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:06:00 +00:00
Franck Nijhof 2776e966ff Reduce Wyoming satellite disconnect log to debug level (#172499) 2026-06-01 16:05:58 +00:00
Franck Nijhof 5f9872886d Convert Roomba hw_version to string for device registry (#172497) 2026-06-01 16:05:56 +00:00
Franck Nijhof f728a1bf09 Add missing Flexit BACnet transient operation modes to preset map (#172493) 2026-06-01 16:05:53 +00:00
Franck Nijhof df65132268 Add prog operating mode to Overkiz Atlantic heater HVAC mapping (#172491) 2026-06-01 16:05:51 +00:00
Michael c13822b776 Handle FileNotFoundError in Immich upload_file action (#172490) 2026-06-01 16:05:49 +00:00
Simone Chemelli c6d696db0c Remove redundant definitions in Alexa Devices (#172488) 2026-06-01 16:05:46 +00:00
Franck Nijhof 114c9bbafa Increase ConfigEntryNotReady retry backoff cap from 80s to 10 minutes (#172487) 2026-06-01 16:05:44 +00:00
Franck Nijhof 323ce99fda Fix Tado config flow crash on device activation polling (#172486) 2026-06-01 16:05:42 +00:00
Jan Bouwhuis 7a7ef85db2 Move MQTT protocol setting to main options (#172482) 2026-06-01 16:05:40 +00:00
Franck Nijhof 7ab402618d Handle DAVError in CalDAV get_supported_components (#172479) 2026-06-01 16:05:37 +00:00
Franck Nijhof aa87295a1e Fix Growatt setup failure on API rate limit (#172472) 2026-06-01 16:05:35 +00:00
Simone Chemelli 3bd979e976 Bump samsungtvws to 3.0.5 (#172471) 2026-06-01 16:05:33 +00:00
Paul Bottein 9dddf76548 Name the Broadlink RF transmitter entity (#172468) 2026-06-01 16:05:31 +00:00
Franck Nijhof 1828579f03 Fix Volvo lock crash when API field is missing from coordinator data (#172465) 2026-06-01 16:05:29 +00:00
Bram Kragten 47bca8d8c2 Bump frontend to 20260527.1 (#172462)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:27 +00:00
Paulus Schoutsen 6f3fb5c7bd Add lg_tv_rs232 to LG brand (#172458)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:24 +00:00
TheJulianJES d9b4b5b3d0 Fix Matter BLE proxy blocking startup (#172456) 2026-06-01 16:05:22 +00:00
Ronald van der Meer 342b364af6 Fix Duco regression where entities become unavailable when LAN info fetch fails (#172448) 2026-06-01 16:05:20 +00:00
Simone Chemelli 951cd71741 Discard old events for Alexa Devices (#172446) 2026-06-01 16:05:18 +00:00
Franck Nijhof e86a54f81c Fix Hue light ZeroDivisionError when mirek value is zero (#172442) 2026-06-01 16:05:15 +00:00
Simone Chemelli ba8b33e1a9 Fix Shelly sensor restore when not initialized (#172441) 2026-06-01 16:05:13 +00:00
Franck Nijhof b6c40ba3fc Fix Jellyfin media source crash when entry is not loaded (#172437) 2026-06-01 16:05:11 +00:00
Franck Nijhof f2f29c07c7 Fix SmartThings light checking wrong component for capabilities (#172430)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-06-01 16:05:08 +00:00
Franck Nijhof 50a3ab115d Fix iZone integration broken by python-izone 1.2.10 API change (#172427) 2026-06-01 16:05:06 +00:00
Franck Nijhof c204054847 Convert yamaha_musiccast sw_version to string (#172411) 2026-06-01 16:05:04 +00:00
Jan Bouwhuis 28d6eab2dd Improve MQTT protocol deprecation repair message (#172404)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:05:02 +00:00
Manu 6b1ee57bd5 Fix index error in DuckDNS integration (#172392) 2026-06-01 16:05:00 +00:00
J. Nick Koston 7247f95b05 Bump onvif-zeep-async to 4.1.1 (#172391) 2026-06-01 16:04:57 +00:00
J. Nick Koston cdeafdfd42 Bump yalexs to 9.2.1 (#172389) 2026-06-01 16:04:55 +00:00
Abílio Costa 9d60fce72e Fix OMIE sensors not updating on setup (#172383) 2026-06-01 16:04:53 +00:00
Simone Chemelli 2e4c6c4370 Bump aioamazondevices to 13.8.1 (#172382) 2026-06-01 16:04:50 +00:00
J. Nick Koston b7e36e297b Bump dbus-fast to 5.0.16 (#172378) 2026-06-01 16:04:48 +00:00
Stefan Agner 7e178efe63 Reject backup uploads with unsafe inner name (#172368)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:04:46 +00:00
puddly 38f25c4b41 Bump ZHA to 1.4.0 (#172357) 2026-06-01 16:04:44 +00:00
torben-iometer 2c2e70a11c bump iometer version to 1.0.1 (#172338) 2026-06-01 16:04:41 +00:00
Linkplay2020 190350aec3 Bump wiim to 1.0.4 (#172334)
Co-authored-by: Tao Jiang <tao.jiang@linkplay.com>
2026-06-01 16:04:39 +00:00
tlpeter a87083b6c1 Bump renault-api to 0.5.11 (#172333)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-06-01 16:04:36 +00:00
Mike Degatano d5be54fd40 Migrate analytics integration to config entry setup (#171801)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-06-01 16:04:34 +00:00
Mike Degatano 46f2ad9eb2 During onboarding, ensure Supervisor is up to date during hassio setup (#171129)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-01 16:04:31 +00:00
mhuiskes add75622d6 Fix zeversolar coordinator to raise UpdateFailed on errors (#170507) 2026-06-01 16:04:29 +00:00
Daniel Feinberg 2f334d657d Fix apple_tv HomePod streaming failures when device is idle (#170033)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 16:04:26 +00:00
Nikhil Deepak fd69d384be Reset MQTT valve opening/closing state at intermediate positions (#165176)
Co-authored-by: jbouwh <jan@jbsoft.nl>
2026-06-01 16:04:24 +00:00
Franck Nijhof fce17c8e6f Bump version to 2026.6.0b0 2026-05-27 16:07:37 +00:00
323 changed files with 2272 additions and 26825 deletions
+33 -2
View File
@@ -8,8 +8,39 @@ description: Reviews GitHub pull requests and provides feedback comments. This i
## Follow these steps:
1. Use 'gh pr view' to get the PR details and description.
2. Use 'gh pr diff' to see all the changes in the PR.
3. Review the changes following the `review` skill. It is VERY IMPORTANT to follow the `review` skill instructions.
4. Check if all existing review comments have been addressed.
3. Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
4. Ensure any existing review comments have been addressed.
5. Generate constructive review comments in the CONSOLE. DO NOT POST TO GITHUB YOURSELF.
## Verification:
- After the review, run parallel subagents for each finding to double check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes
- Be constructive and specific in your comments
- Suggest improvements where appropriate
- Only provide review feedback in the CONSOLE. DO NOT ACT ON GITHUB.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] sensor.py:143 - Memory leak
- [PROBLEM] data_processing.py:87 - Inefficient algorithm
- [SUGGESTION] test_init.py:45 - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -24,7 +24,6 @@ The following platforms have extra guidelines:
## Entity platforms
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
## Integration Quality Scale
-40
View File
@@ -1,40 +0,0 @@
---
name: review
description: Reviews code changes and provides constructive feedback. Should be used when a review is requested to provide a consistent review behavior and output format. This skill can be used for code reviews in general, not just for GitHub pull requests.
---
# Review Code Changes
## Analyze the code changes for:
- Code quality and style consistency
- Potential bugs or issues
- Performance implications
- Security concerns
- Test coverage
- Documentation updates if needed
## Verification:
- After the review, run parallel subagents for each finding to double-check it.
- Spawn up to a maximum of 10 parallel subagents at a time.
- Gather the results from the subagents and summarize them in the final review comments.
## IMPORTANT:
- Just review. DO NOT make any changes.
- Be constructive and specific in your comments.
- Suggest improvements where appropriate.
- Do not comment on code style, formatting, or linting-only issues.
- No need to run tests or linters, just review the code changes.
- No need to highlight things that are already good.
## Output format:
- List specific comments for each file/line that needs attention.
- Reference files using a markdown link whose text is the file name and line number, and whose target is the full relative path including the line number (e.g. `[sensor.py:143](homeassistant/components/example/sensor.py:143)`).
- In the end, summarize with an overall assessment (approve, request changes, or comment) and bullet point list of changes suggested, if any.
- Example output:
```
Overall assessment: request changes.
- [CRITICAL] [sensor.py:143](homeassistant/components/example/sensor.py:143) - Memory leak
- [PROBLEM] [data_processing.py:87](homeassistant/components/example/data_processing.py:87) - Inefficient algorithm
- [SUGGESTION] [test_init.py:45](tests/components/example/test_init.py:45) - Improve x variable name
```
- Make sure to include the file and line number when possible in the bullet points.
@@ -27,7 +27,6 @@ The following platforms have extra guidelines:
## Entity platforms
- Ensure `async_added_to_hass()` and `async_will_remove_from_hass()` have symmetrical behavior. For example, if a subscription is created in `async_added_to_hass()`, it should be unsubscribed in `async_will_remove_from_hass()`. Also, if something is torn down in `async_will_remove_from_hass()`, it should be set up in `async_added_to_hass()`.
- Entity base class (e.g. `SensorEntity`, `TrackerEntity`) provide a stable API for child classes to inherit from. Do not suggest redeclaring or duplicating attributes, properties, or methods the base class already provides, and do not add guards against the parent's behavior changing — rely on the base class instead.
## Integration Quality Scale
+7 -7
View File
@@ -344,13 +344,13 @@ jobs:
- name: Login to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -380,7 +380,7 @@ jobs:
# 2025.12.0.dev202511250240 -> tags: 2025.12.0.dev202511250240, dev
- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with:
images: ${{ matrix.registry }}/home-assistant
sep-tags: ","
@@ -394,7 +394,7 @@ jobs:
type=semver,pattern={{major}}.{{minor}},value=${{ needs.init.outputs.version }},enable=${{ !contains(needs.init.outputs.version, 'd') && !contains(needs.init.outputs.version, 'b') }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v3.7.1
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v3.7.1
- name: Copy architecture images to DockerHub
if: matrix.registry == 'docker.io/homeassistant'
@@ -523,14 +523,14 @@ jobs:
persist-credentials: false
- name: Login to GitHub Container Registry
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
@@ -543,7 +543,7 @@ jobs:
- name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile
+7 -7
View File
@@ -36,7 +36,7 @@
# - actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
# - actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
# - actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
# - github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
# - github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
#
# Container images used:
# - ghcr.io/github/gh-aw-firewall/agent:0.25.46
@@ -90,7 +90,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -352,7 +352,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -961,7 +961,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1100,7 +1100,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1325,7 +1325,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
@@ -1383,7 +1383,7 @@ jobs:
steps:
- name: Setup Scripts
id: setup
uses: github/gh-aw-actions/setup@f889c9c3c06adeaabccefc06e29c42733ee05dff # v0.75.0
uses: github/gh-aw-actions/setup@d3abfe96a194bce3a523ed2093ddedd5704cdf62 # v0.74.4
with:
destination: ${{ runner.temp }}/gh-aw/actions
job-name: ${{ github.job }}
+1 -1
View File
@@ -39,7 +39,7 @@ on:
env:
CACHE_VERSION: 3
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.7"
HA_SHORT_VERSION: "2026.6"
ADDITIONAL_PYTHON_VERSIONS: "[]"
# 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
category: "/language:python"
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
issues: write # To lock issues
pull-requests: write # To lock pull requests
steps:
- uses: dessant/lock-threads@851cffe46851ddd2051ea7147ebdc995113241c3 # v6.0.1
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
github-token: ${{ github.token }}
issue-inactive-days: "30"
+1 -2
View File
@@ -20,7 +20,6 @@ jobs:
permissions:
issues: write # To label and close stale issues
pull-requests: write # To label and close stale PRs
actions: write # To delete stalebot state
steps:
# The 60 day stale policy for PRs
# Used for:
@@ -59,7 +58,7 @@ jobs:
# v3.2.0
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
app-id: ${{ secrets.ISSUE_TRIAGE_APP_ID }} # zizmor: ignore[secrets-outside-env]
private-key: ${{ secrets.ISSUE_TRIAGE_APP_PEM }} # zizmor: ignore[secrets-outside-env]
# The 90 day stale policy for issues
+4 -2
View File
@@ -92,7 +92,8 @@ def _extract_backup(
):
ostf.tar.extractall(
path=Path(tempdir, "extracted"),
filter="tar",
members=securetar.secure_path(ostf.tar),
filter="fully_trusted",
)
backup_meta_file = Path(tempdir, "extracted", "backup.json")
backup_meta = json.loads(backup_meta_file.read_text(encoding="utf8"))
@@ -118,7 +119,8 @@ def _extract_backup(
) as istf:
istf.extractall(
path=Path(tempdir, "homeassistant"),
filter="tar",
members=securetar.secure_path(istf),
filter="fully_trusted",
)
if restore_content.restore_homeassistant:
keep = list(KEEP_BACKUPS)
@@ -8,6 +8,7 @@ from bleak.backends.device import BLEDevice
from bleak_retry_connector import close_stale_connections_by_address
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
@@ -63,7 +64,16 @@ class AirthingsBLEDataUpdateCoordinator(DataUpdateCoordinator[AirthingsDevice]):
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find Airthings device with address {address}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
self.hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
self.ble_device = ble_device
@@ -54,5 +54,10 @@
"name": "Radon longterm level"
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find Airthings device with address {address}: {reason}"
}
}
}
+1 -1
View File
@@ -372,7 +372,7 @@ def _convert_content( # noqa: C901
)
if (
content.native.container is not None
and content.native.container.expires_at > datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
and content.native.container.expires_at > datetime.now(UTC)
):
container_id = content.native.container.id
@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.7", "yalexs-ble==3.3.0"]
"requirements": ["yalexs==9.2.1", "yalexs-ble==3.3.0"]
}
@@ -21,6 +21,6 @@
"bluetooth-auto-recovery==1.6.4",
"bluetooth-data-tools==1.29.18",
"dbus-fast==5.0.16",
"habluetooth==6.8.0"
"habluetooth==6.8.1"
]
}
@@ -24,7 +24,6 @@ from homeassistant.components.alexa import (
entities as alexa_entities,
errors as alexa_errors,
)
from homeassistant.components.frontend import DATA_THEMES
from homeassistant.components.google_assistant import helpers as google_helpers
from homeassistant.components.homeassistant import exposed_entities
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
@@ -509,15 +508,6 @@ class DownloadSupportPackageView(HomeAssistantView):
"custom_integrations": custom_integrations,
}
@callback
def _get_themes_info(self, hass: HomeAssistant) -> dict[str, Any]:
"""Collect information about user-installed custom themes."""
themes: dict[str, Any] = hass.data.get(DATA_THEMES, {})
return {
"count": len(themes),
"themes": sorted(themes),
}
async def _generate_markdown(
self,
hass: HomeAssistant,
@@ -579,25 +569,6 @@ class DownloadSupportPackageView(HomeAssistantView):
)
markdown += "\n</details>\n\n"
# Add custom themes information
try:
themes_info = self._get_themes_info(hass)
except Exception: # noqa: BLE001
# Broad exception catch for robustness in support package generation
markdown += "## Custom Themes\n\n"
markdown += "Unable to collect themes information\n\n"
else:
markdown += "## Custom Themes\n\n"
markdown += f"Custom themes: {themes_info['count']}\n\n"
if themes_info["themes"]:
markdown += "<details><summary>Custom themes</summary>\n\n"
markdown += "Name\n"
markdown += "---\n"
for theme in themes_info["themes"]:
markdown += f"{theme}\n"
markdown += "\n</details>\n\n"
for domain, domain_info in domains_info.items():
domain_info_md = get_domain_table_markdown(domain_info)
markdown += (
@@ -12,19 +12,13 @@ from homeassistant.const import (
CONF_DOMAIN,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_OPTIONS,
CONF_PLATFORM,
CONF_TYPE,
CONF_ZONE,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.trigger import (
TriggerActionType,
TriggerInfo,
# protected, but only used for legacy triggers
_async_attach_trigger_cls,
)
from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -85,18 +79,16 @@ async def async_attach_trigger(
event = zone.EVENT_ENTER
else:
event = zone.EVENT_LEAVE
zone_config = await zone.LegacyZoneTrigger.async_validate_config(
hass,
{
CONF_OPTIONS: {
CONF_ENTITY_ID: [config[CONF_ENTITY_ID]],
CONF_ZONE: config[CONF_ZONE],
CONF_EVENT: event,
}
},
)
return await _async_attach_trigger_cls(
hass, zone.LegacyZoneTrigger, "device", zone_config, action, trigger_info
zone_config = {
CONF_PLATFORM: ZONE_DOMAIN,
CONF_ENTITY_ID: config[CONF_ENTITY_ID],
CONF_ZONE: config[CONF_ZONE],
CONF_EVENT: event,
}
zone_config = await zone.async_validate_trigger_config(hass, zone_config)
return await zone.async_attach_trigger(
hass, zone_config, action, trigger_info, platform_type="device"
)
@@ -1,7 +1,6 @@
"""Provide functionality to keep track of devices."""
import asyncio
import logging
from typing import TYPE_CHECKING, Any, final
from propcache.api import cached_property
@@ -23,7 +22,6 @@ from homeassistant.core import (
EventStateChangedData,
HomeAssistant,
State,
async_get_hass_or_none,
callback,
)
from homeassistant.helpers import (
@@ -39,7 +37,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.loader import async_suggest_report_issue
from homeassistant.util.hass_dict import HassKey
from .const import (
@@ -55,8 +52,6 @@ from .const import (
SourceType,
)
_LOGGER = logging.getLogger(__name__)
DATA_KEY: HassKey[dict[str, tuple[str, str]]] = HassKey(f"{DOMAIN}_mac")
@@ -169,35 +164,11 @@ class BaseTrackerEntity(Entity):
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_source_type: SourceType
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "battery_level" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated battery_level property on "
"a subclass of BaseTrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def battery_level(self) -> int | None:
"""Return the battery level of the device.
Percentage from 0-100.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
return None
@@ -241,38 +212,13 @@ class TrackerEntity(
_attr_in_zones: list[str] | None = None
_attr_latitude: float | None = None
_attr_location_accuracy: float = 0
# _attr_location_name is deprecated and will be removed in Home Assistant 2027.7
_attr_location_name: str | None = None
_attr_longitude: float | None = None
_attr_source_type: SourceType = SourceType.GPS
__active_zone: State | None = None
# If we reported setting deprecated _attr_location_name
__deprecated_attr_location_name_reported = False
__in_zones: list[str] | None = None
def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing."""
super().__init_subclass__(**kwargs)
if "location_name" in cls.__dict__:
if cls.__module__.startswith("homeassistant.components."):
# Don't ask users to report issue for built in integrations,
# they already have issues opened on them.
return
report_issue = async_suggest_report_issue(
async_get_hass_or_none(), module=cls.__module__
)
_LOGGER.warning(
(
"%s::%s is overriding the deprecated location_name property on "
"an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
cls.__module__,
cls.__name__,
report_issue,
)
@cached_property
def should_poll(self) -> bool:
"""No polling for entities that have location pushed."""
@@ -303,32 +249,7 @@ class TrackerEntity(
@cached_property
def location_name(self) -> str | None:
"""Return a location name for the current location of the device.
The property is deprecated and will be removed in Home Assistant 2027.7.
"""
if (location_name := self._attr_location_name) is not None:
if (
not self.__deprecated_attr_location_name_reported
and not self.__class__.__module__.startswith(
"homeassistant.components."
)
):
report_issue = async_suggest_report_issue(
self.hass, module=self.__class__.__module__
)
_LOGGER.warning(
(
"%s::%s is setting the deprecated _attr_location_name attribute "
"on an instance of TrackerEntity, this will be unsupported from "
"Home Assistant 2027.7, please %s"
),
self.__class__.__module__,
self.__class__.__name__,
report_issue,
)
self.__deprecated_attr_location_name_reported = True
return location_name
"""Return a location name for the current location of the device."""
return self._attr_location_name
@cached_property
@@ -100,7 +100,7 @@ class DwdWeatherWarningsSensor(
if warnings is None:
return []
now = datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
now = datetime.now(UTC)
return [warning for warning in warnings if warning[API_ATTR_WARNING_END] > now]
@property
@@ -106,7 +106,7 @@ async def async_migrate_entry(
new_options = {**config_entry.options}
if config_entry.minor_version < 2:
# Add defaults only if they're not already present
# Add defaults only if theyre not already present
if "stt_auto_language" not in new_options:
new_options["stt_auto_language"] = False
if "stt_model" not in new_options:
@@ -221,7 +221,6 @@ def update_listeners(hass: HomeAssistant, entry: EnergyIDConfigEntry) -> None:
):
try:
value = float(current_state.state)
# pylint: disable-next=home-assistant-enforce-utcnow
timestamp = current_state.last_updated or dt.datetime.now(dt.UTC)
client.get_or_create_sensor(energyid_key).update(value, timestamp)
except ValueError, TypeError:
@@ -3,7 +3,7 @@
from datetime import timedelta
import logging
from env_canada import ECAirQuality, ECMap, ECWeather
from env_canada import ECAirQuality, ECRadar, ECWeather
from homeassistant.const import CONF_LANGUAGE, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant
@@ -43,7 +43,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ECConfigEntry) ->
errors = errors + 1
_LOGGER.warning("Unable to retrieve Environment Canada weather")
radar_data = ECMap(coordinates=(lat, lon), layer="precip_type", legend=False)
radar_data = ECRadar(coordinates=(lat, lon))
radar_coordinator = ECDataUpdateCoordinator(
hass, config_entry, radar_data, "radar", DEFAULT_RADAR_UPDATE_INTERVAL
)
@@ -1,6 +1,6 @@
"""Support for the Environment Canada radar imagery."""
from env_canada import ECMap
from env_canada import ECRadar
import voluptuous as vol
from homeassistant.components.camera import Camera
@@ -11,20 +11,13 @@ from homeassistant.helpers.entity_platform import (
)
from homeassistant.helpers.typing import VolDictType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import ATTR_OBSERVATION_TIME
from .coordinator import ECConfigEntry, ECDataUpdateCoordinator
SERVICE_SET_RADAR_TYPE = "set_radar_type"
SET_RADAR_TYPE_SCHEMA: VolDictType = {
vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow", "Precipitation type"]),
}
_RADAR_TYPE_TO_LAYER: dict[str, str] = {
"Rain": "rain",
"Snow": "snow",
"Precipitation type": "precip_type",
vol.Required("radar_type"): vol.In(["Auto", "Rain", "Snow"]),
}
@@ -45,13 +38,13 @@ async def async_setup_entry(
)
class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECMap]], Camera):
class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECRadar]], Camera):
"""Implementation of an Environment Canada radar camera."""
_attr_has_entity_name = True
_attr_translation_key = "radar"
def __init__(self, coordinator: ECDataUpdateCoordinator[ECMap]) -> None:
def __init__(self, coordinator: ECDataUpdateCoordinator[ECRadar]) -> None:
"""Initialize the camera."""
super().__init__(coordinator)
Camera.__init__(self)
@@ -83,13 +76,6 @@ class ECCameraEntity(CoordinatorEntity[ECDataUpdateCoordinator[ECMap]], Camera):
async def async_set_radar_type(self, radar_type: str) -> None:
"""Set the type of radar to retrieve."""
if radar_type == "Auto":
# Choose rain for months April through October, snow otherwise
layer = "rain" if dt_util.now().month in range(4, 11) else "snow"
else:
layer = _RADAR_TYPE_TO_LAYER[radar_type]
# Apply new layer and clear cache to force refresh
self.radar_object.layer = layer
self.radar_object.clear_cache()
await self.coordinator.async_request_refresh()
self.radar_object.precip_type = radar_type.lower()
await self.radar_object.update()
@@ -5,7 +5,7 @@ from datetime import timedelta
import logging
import xml.etree.ElementTree as ET
from env_canada import ECAirQuality, ECMap, ECWeather, ECWeatherUpdateFailed, ec_exc
from env_canada import ECAirQuality, ECRadar, ECWeather, ECWeatherUpdateFailed, ec_exc
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -17,7 +17,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type ECConfigEntry = ConfigEntry[ECRuntimeData]
type ECDataType = ECAirQuality | ECMap | ECWeather
type ECDataType = ECAirQuality | ECRadar | ECWeather
@dataclass
@@ -25,7 +25,7 @@ class ECRuntimeData:
"""Class to hold EC runtime data."""
aqhi_coordinator: ECDataUpdateCoordinator[ECAirQuality]
radar_coordinator: ECDataUpdateCoordinator[ECMap]
radar_coordinator: ECDataUpdateCoordinator[ECRadar]
weather_coordinator: ECDataUpdateCoordinator[ECWeather]
@@ -12,11 +12,10 @@ set_radar_type:
fields:
radar_type:
required: true
example: Rain
example: Snow
selector:
select:
options:
- "Auto"
- "Rain"
- "Snow"
- "Precipitation type"
@@ -8,13 +8,14 @@ from eq3btsmart import Thermostat
from eq3btsmart.exceptions import Eq3Exception
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .const import DOMAIN, SIGNAL_THERMOSTAT_CONNECTED, SIGNAL_THERMOSTAT_DISCONNECTED
from .models import Eq3Config, Eq3ConfigEntryData
PLATFORMS = [
@@ -49,7 +50,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: Eq3ConfigEntry) -> bool:
if device is None:
raise ConfigEntryNotReady(
f"[{eq3_config.mac_address}] Device could not be found"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"mac_address": eq3_config.mac_address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
mac_address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
thermostat = Thermostat(device)
@@ -61,5 +61,10 @@
"name": "Lock"
}
}
},
"exceptions": {
"device_not_found": {
"message": "[{mac_address}] Device could not be found: {reason}"
}
}
}
+1 -1
View File
@@ -161,7 +161,7 @@ class EvoChild(EvoEntity):
or self._schedule is None
or (
(until := self._setpoints.get("next_sp_from")) is not None
and until < datetime.now(UTC) # pylint: disable=home-assistant-enforce-utcnow
and until < datetime.now(UTC)
)
): # must use self._setpoints, not self.setpoints
await get_schedule()
@@ -91,7 +91,6 @@ class TokenManager(AbstractTokenManager, AbstractSessionManager):
session_id_expires = session.get(SZ_SESSION_ID_EXPIRES)
if session_id_expires is None:
# pylint: disable-next=home-assistant-enforce-utcnow
self._session_id_expires = datetime.now(tz=UTC) + timedelta(minutes=15)
else:
self._session_id_expires = datetime.fromisoformat(session_id_expires)
+1 -1
View File
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import FlussConfigEntry, FlussDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BUTTON, Platform.COVER]
PLATFORMS: list[Platform] = [Platform.BUTTON]
async def async_setup_entry(
-1
View File
@@ -6,4 +6,3 @@ import logging
DOMAIN = "fluss"
LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(minutes=30)
COMMAND_REFRESH_COOLDOWN = 10
+12 -20
View File
@@ -13,11 +13,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import slugify
from .const import COMMAND_REFRESH_COOLDOWN, LOGGER, UPDATE_INTERVAL
from .const import LOGGER, UPDATE_INTERVAL
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
@@ -36,24 +35,18 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
name=f"Fluss+ ({slugify(api_key[:8])})",
config_entry=config_entry,
update_interval=UPDATE_INTERVAL,
request_refresh_debouncer=Debouncer(
hass,
LOGGER,
cooldown=COMMAND_REFRESH_COOLDOWN,
immediate=False,
),
)
async def _async_get_status(self, device_id: str) -> dict[str, Any]:
"""Return per-device status."""
async def _async_get_connectivity(self, device_id: str) -> bool:
"""Return connectivity for a device; False if the status call fails."""
try:
response = await self.api.async_get_device_status(device_id)
except FlussApiClientError as err:
raise UpdateFailed(f"Error fetching status for {device_id}: {err}") from err
return response["status"]
status = await self.api.async_get_device_status(device_id)
except FlussApiClientError:
return False
return status["status"]["internetConnected"]
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Fetch Fluss+ devices and merge per-device status."""
"""Fetch Fluss+ devices and merge per-device connectivity status."""
try:
devices = await self.api.async_get_devices()
except FlussApiClientAuthenticationError as err:
@@ -66,11 +59,10 @@ class FlussDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]
for device in devices["devices"]
if device["userPermissions"]["canUseWiFi"]
]
statuses = await asyncio.gather(
*(self._async_get_status(d["deviceId"]) for d in device_list)
connectivity = await asyncio.gather(
*(self._async_get_connectivity(d["deviceId"]) for d in device_list)
)
return {
device["deviceId"]: {**device, **status}
for device, status in zip(device_list, statuses, strict=False)
device["deviceId"]: {**device, "internetConnected": connected}
for device, connected in zip(device_list, connectivity, strict=False)
}
-89
View File
@@ -1,89 +0,0 @@
"""Cover platform for Fluss+ devices that report an open/closed status."""
from typing import Any
from homeassistant.components.cover import (
CoverDeviceClass,
CoverEntity,
CoverEntityFeature,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import FlussApiClientError, FlussConfigEntry
from .entity import FlussEntity
PARALLEL_UPDATES = 0
STATUS_OPEN = "Open"
STATUS_CLOSED = "Closed"
async def async_setup_entry(
hass: HomeAssistant,
entry: FlussConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Fluss covers for devices that report an open/closed status."""
coordinator = entry.runtime_data
added_device_ids: set[str] = set()
def _async_add_new_entities() -> None:
new_entities = [
FlussCover(coordinator, device_id, device)
for device_id, device in coordinator.data.items()
if "openCloseStatus" in device and device_id not in added_device_ids
]
if not new_entities:
return
added_device_ids.update(entity.device_id for entity in new_entities)
async_add_entities(new_entities)
_async_add_new_entities()
entry.async_on_unload(coordinator.async_add_listener(_async_add_new_entities))
class FlussCover(FlussEntity, CoverEntity):
"""Representation of a Fluss+ cover."""
_attr_device_class = CoverDeviceClass.GARAGE
_attr_name = None
_attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
@property
def available(self) -> bool:
"""Return True only when the device is online."""
return super().available and self.device["internetConnected"]
@property
def is_closed(self) -> bool | None:
"""Return whether the cover is closed."""
status = self.device.get("openCloseStatus")
if status == STATUS_CLOSED:
return True
if status == STATUS_OPEN:
return False
return None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
try:
await self.coordinator.api.async_open_device(self.device_id)
except FlussApiClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="command_failed"
) from err
await self.coordinator.async_request_refresh()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the cover."""
try:
await self.coordinator.api.async_close_device(self.device_id)
except FlussApiClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="command_failed"
) from err
await self.coordinator.async_request_refresh()
@@ -19,10 +19,5 @@
"description": "Your Fluss API key, available in the profile page of the Fluss+ app"
}
}
},
"exceptions": {
"command_failed": {
"message": "Failed to send command to Fluss+ device"
}
}
}
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260527.1"]
"requirements": ["home-assistant-frontend==20260527.2"]
}
@@ -320,7 +320,7 @@ class AFSAPIDevice(MediaPlayerEntity):
@fs_command_exception_wrap
async def async_media_play(self) -> None:
"""Send play command."""
if (await self.fs_device.get_play_state()) == PlayState.STOPPED:
if (await self.fs_device.get_play_status()) == PlayState.STOPPED:
# The 'play' command only seems to work when the current stream is paused.
# We need to send a 'stop' command instead to resume a stopped stream.
await self.fs_device.stop()
@@ -279,7 +279,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
super()._handle_coordinator_update()
return
time = datetime.now(UTC) + timedelta(seconds=value) # pylint: disable=home-assistant-enforce-utcnow
time = datetime.now(UTC) + timedelta(seconds=value)
if not self._attr_native_value:
self._attr_native_value = time
super()._handle_coordinator_update()
@@ -199,7 +199,6 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = {
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.RECEIVER): TYPE_RECEIVER,
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.SPEAKER): TYPE_SPEAKER,
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.TV): TYPE_TV,
(media_player.DOMAIN, media_player.MediaPlayerDeviceClass.PROJECTOR): TYPE_TV,
(sensor.DOMAIN, sensor.SensorDeviceClass.AQI): TYPE_SENSOR,
(sensor.DOMAIN, sensor.SensorDeviceClass.HUMIDITY): TYPE_SENSOR,
(sensor.DOMAIN, sensor.SensorDeviceClass.TEMPERATURE): TYPE_SENSOR,
@@ -2728,11 +2728,7 @@ class ChannelTrait(_Trait):
if (
domain == media_player.DOMAIN
and (features & MediaPlayerEntityFeature.PLAY_MEDIA)
and device_class
in (
media_player.MediaPlayerDeviceClass.TV,
media_player.MediaPlayerDeviceClass.PROJECTOR,
)
and device_class == media_player.MediaPlayerDeviceClass.TV
):
return True
@@ -37,7 +37,7 @@ class GreenPlanetEnergySensorEntityDescription(SensorEntityDescription):
def _get_lowest_price_day_time(
api: GreenPlanetEnergyAPI, data: dict[str, Any]
) -> datetime | None:
"""Return timestamp of the lowest-priced day hour (06:00-18:00)."""
"""Return timestamp of the lowest-priced day hour (06:0018:00)."""
now = dt_util.now()
now_h = now.hour
hour = api.get_lowest_price_day_with_hour(data, now_h)[1]
+13 -34
View File
@@ -1,7 +1,7 @@
"""The homee cover platform."""
import logging
from typing import TYPE_CHECKING, Any, cast
from typing import Any, cast
from pyHomee.const import AttributeType, NodeProfile
from pyHomee.model import HomeeAttribute, HomeeNode
@@ -35,12 +35,6 @@ COVER_DEVICE_PROFILES = {
NodeProfile.ENTRANCE_GATE_OPERATOR: CoverDeviceClass.GATE,
NodeProfile.SHUTTER_POSITION_SWITCH: CoverDeviceClass.SHUTTER,
}
IS_CLOSED_ATTRIBUTES = [
AttributeType.OPEN_CLOSE,
AttributeType.UP_DOWN,
AttributeType.POSITION,
AttributeType.SHUTTER_SLAT_POSITION,
]
def get_open_close_attribute(node: HomeeNode) -> HomeeAttribute | None:
@@ -89,23 +83,9 @@ async def add_cover_entities(
nodes: list[HomeeNode],
) -> None:
"""Add homee cover entities."""
entities: list[HomeeNode] = []
for node in nodes:
if is_cover_node(node):
if any(
node.get_attribute_by_type(attr) is not None
for attr in IS_CLOSED_ATTRIBUTES
):
entities.append(node)
else:
_LOGGER.warning(
"Cover %s could not be added, because it is missing an Attribute "
"for closed indication. Please open an issue at "
"https://github.com/home-assistant/core/issues",
node.name,
)
async_add_entities(HomeeCover(cover, config_entry) for cover in entities)
async_add_entities(
HomeeCover(node, config_entry) for node in nodes if is_cover_node(node)
)
async def async_setup_entry(
@@ -207,7 +187,7 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
return None
@property
def is_closed(self) -> bool:
def is_closed(self) -> bool | None:
"""Return if the cover is closed."""
if (
attribute := self._node.get_attribute_by_type(AttributeType.POSITION)
@@ -220,16 +200,15 @@ class HomeeCover(HomeeNodeEntity, CoverEntity):
return self._open_close_attribute.get_value() == 0
# If none of the above is present, it will be a slat only cover.
attribute = self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
if TYPE_CHECKING:
# This case should not happen, because we check for
# the presence of an IS_CLOSED_ATTRIBUTE when adding entities.
assert attribute is not None
# If none of the above is present, it might be a slat only cover.
if (
attribute := self._node.get_attribute_by_type(
AttributeType.SHUTTER_SLAT_POSITION
)
) is not None:
return attribute.get_value() == attribute.minimum
return attribute.get_value() == attribute.minimum
return None
async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
@@ -202,10 +202,7 @@ def get_accessory( # noqa: C901
if device_class == MediaPlayerDeviceClass.RECEIVER:
a_type = "ReceiverMediaPlayer"
elif device_class in (
MediaPlayerDeviceClass.TV,
MediaPlayerDeviceClass.PROJECTOR,
):
elif device_class == MediaPlayerDeviceClass.TV:
a_type = "TelevisionMediaPlayer"
elif validate_media_player_features(state, feature_list):
a_type = "MediaPlayer"
+1 -5
View File
@@ -695,11 +695,7 @@ def state_needs_accessory_mode(state: State) -> bool:
return (
state.domain == MEDIA_PLAYER_DOMAIN
and state.attributes.get(ATTR_DEVICE_CLASS)
in (
MediaPlayerDeviceClass.TV,
MediaPlayerDeviceClass.RECEIVER,
MediaPlayerDeviceClass.PROJECTOR,
)
in (MediaPlayerDeviceClass.TV, MediaPlayerDeviceClass.RECEIVER)
) or (
state.domain == REMOTE_DOMAIN
and state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
@@ -6,6 +6,7 @@ from bleak import BleakError
from bleak_retry_connector import close_stale_connections_by_address, get_device
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ADDRESS, CONF_CLIENT_ID, CONF_PIN, Platform
from homeassistant.core import HomeAssistant
@@ -56,7 +57,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: HusqvarnaConfigEntry) ->
)
except (TimeoutError, BleakError) as exception:
raise ConfigEntryNotReady(
f"Unable to connect to device {address} due to {exception}"
translation_domain=DOMAIN,
translation_key="connection_failed",
translation_placeholders={
"address": address,
"error": str(exception) or type(exception).__name__,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
) from exception
LOGGER.debug("connected and paired")
@@ -45,6 +45,9 @@
}
},
"exceptions": {
"connection_failed": {
"message": "Unable to connect to device {address} due to {error}: {reason}"
},
"pin_required": {
"message": "PIN is required for {domain_name}"
}
@@ -59,6 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IcloudConfigEntry) -> bo
await hass.async_add_executor_job(account.setup)
entry.runtime_data = account
entry.async_on_unload(account.cancel_fetch)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+9 -1
View File
@@ -92,6 +92,7 @@ class IcloudAccount:
self._retried_fetch = False
self._config_entry = config_entry
self._unsub_fetch: CALLBACK_TYPE | None = None
self.listeners: list[CALLBACK_TYPE] = []
def setup(self) -> None:
@@ -293,9 +294,16 @@ class IcloudAccount:
self._max_interval,
)
def cancel_fetch(self) -> None:
"""Cancel the scheduled fetch timer."""
if self._unsub_fetch is not None:
self._unsub_fetch()
self._unsub_fetch = None
def _schedule_next_fetch(self) -> None:
self.cancel_fetch()
if not self._config_entry.pref_disable_polling:
track_point_in_utc_time(
self._unsub_fetch = track_point_in_utc_time(
self.hass,
self.keep_alive,
utcnow() + timedelta(minutes=self._fetch_interval),
@@ -7,9 +7,11 @@ from typing import Any
from inkbird_ble import INKBIRDBluetoothDeviceData, SensorUpdate
from homeassistant.components.bluetooth import (
BluetoothReachabilityIntent,
BluetoothScanningMode,
BluetoothServiceInfo,
BluetoothServiceInfoBleak,
async_address_reachability_diagnostics,
async_ble_device_from_address,
async_last_service_info,
)
@@ -84,7 +86,14 @@ class INKBIRDActiveBluetoothProcessorCoordinator(
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="no_advertisement",
translation_placeholders={"address": self.address},
translation_placeholders={
"address": self.address,
"reason": async_address_reachability_diagnostics(
self.hass,
self.address.upper(),
BluetoothReachabilityIntent.ACTIVE_ADVERTISEMENT,
),
},
)
await self._data.async_start(service_info, service_info.device)
self._entry.async_on_unload(self._data.async_stop)
@@ -20,7 +20,7 @@
},
"exceptions": {
"no_advertisement": {
"message": "The device with address {address} is not advertising; Make sure it is in range and powered on."
"message": "The device with address {address} is not advertising: {reason}"
}
}
}
@@ -339,7 +339,6 @@ class IntegrationSensor(RestoreSensor):
else max_sub_interval
)
self._max_sub_interval_exceeded_callback: CALLBACK_TYPE = lambda *args: None
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time: datetime = datetime.now(tz=UTC)
self._last_integration_trigger = _IntegrationTrigger.StateEvent
self._attr_suggested_display_precision = round_digits or 2
@@ -499,7 +498,6 @@ class IntegrationSensor(RestoreSensor):
old_timestamp, new_timestamp, old_state, new_state
)
self._last_integration_trigger = _IntegrationTrigger.StateEvent
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time = datetime.now(tz=UTC)
finally:
# When max_sub_interval exceeds without state change the source is assumed
@@ -608,7 +606,6 @@ class IntegrationSensor(RestoreSensor):
self._update_integral(area)
self.async_write_ha_state()
# pylint: disable-next=home-assistant-enforce-utcnow
self._last_integration_time = datetime.now(tz=UTC)
self._last_integration_trigger = _IntegrationTrigger.TimeElapsed
+4 -4
View File
@@ -9,14 +9,14 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import DATA_CONFIG, DOMAIN
from .const import DATA_CONFIG, IZONE
from .discovery import async_start_discovery_service, async_stop_discovery_service
PLATFORMS = [Platform.CLIMATE]
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
IZONE: vol.Schema(
{
vol.Optional(CONF_EXCLUDE, default=[]): vol.All(
cv.ensure_list, [cv.string]
@@ -32,13 +32,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Register the iZone component config."""
# Check for manually added config, this may exclude some devices
if conf := config.get(DOMAIN):
if conf := config.get(IZONE):
hass.data[DATA_CONFIG] = conf
# Explicitly added in the config file, create a config entry.
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
IZONE, context={"source": config_entries.SOURCE_IMPORT}
)
)
+4 -4
View File
@@ -43,7 +43,7 @@ from .const import (
DISPATCH_CONTROLLER_RECONNECTED,
DISPATCH_CONTROLLER_UPDATE,
DISPATCH_ZONE_UPDATE,
DOMAIN,
IZONE,
)
type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], _R]
@@ -188,7 +188,7 @@ class ControllerDevice(ClimateEntity):
self._attr_unique_id = controller.device_uid
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, controller.device_uid)},
identifiers={(IZONE, controller.device_uid)},
manufacturer="IZone",
model=controller.sys_type,
name=f"iZone Controller {controller.device_uid}",
@@ -484,12 +484,12 @@ class ZoneDevice(ClimateEntity):
assert controller.unique_id
self._attr_device_info = DeviceInfo(
identifiers={
(DOMAIN, controller.unique_id, zone.index) # type:ignore[arg-type]
(IZONE, controller.unique_id, zone.index) # type:ignore[arg-type]
},
manufacturer="IZone",
model=zone.type.name.title(),
name=zone.name.title(),
via_device=(DOMAIN, controller.unique_id),
via_device=(IZONE, controller.unique_id),
)
async def async_added_to_hass(self) -> None:
@@ -8,7 +8,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_entry_flow
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import DISPATCH_CONTROLLER_DISCOVERED, DOMAIN, TIMEOUT_DISCOVERY
from .const import DISPATCH_CONTROLLER_DISCOVERED, IZONE, TIMEOUT_DISCOVERY
from .discovery import async_start_discovery_service, async_stop_discovery_service
_LOGGER = logging.getLogger(__name__)
@@ -39,4 +39,4 @@ async def _async_has_devices(hass: HomeAssistant) -> bool:
return True
config_entry_flow.register_discovery_flow(DOMAIN, "iZone Aircon", _async_has_devices)
config_entry_flow.register_discovery_flow(IZONE, "iZone Aircon", _async_has_devices)
+1 -1
View File
@@ -1,6 +1,6 @@
"""Constants used by the izone component."""
DOMAIN = "izone"
IZONE = "izone"
DATA_DISCOVERY_SERVICE = "izone_discovery"
DATA_CONFIG = "izone_config"
@@ -10,11 +10,13 @@ from bleak_retry_connector import (
from ld2410_ble import LD2410BLE
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import LD2410BLECoordinator
from .models import LD2410BLEConfigEntry, LD2410BLEData
@@ -34,7 +36,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: LD2410BLEConfigEntry) ->
) or await get_device(address)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find LD2410B device with address {address}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
ld2410_ble = LD2410BLE(ble_device)
@@ -97,5 +97,10 @@
"name": "Static target energy"
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find LD2410B device with address {address}: {reason}"
}
}
}
+12 -2
View File
@@ -5,12 +5,13 @@ import asyncio
from led_ble import LEDBLE
from homeassistant.components import bluetooth
from homeassistant.components.bluetooth import BluetoothReachabilityIntent
from homeassistant.components.bluetooth.match import ADDRESS, BluetoothCallbackMatcher
from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DEVICE_TIMEOUT
from .const import DEVICE_TIMEOUT, DOMAIN
from .coordinator import LEDBLEConfigEntry, LEDBLECoordinator, LEDBLEData
PLATFORMS: list[Platform] = [Platform.LIGHT]
@@ -22,7 +23,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: LEDBLEConfigEntry) -> bo
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
if not ble_device:
raise ConfigEntryNotReady(
f"Could not find LED BLE device with address {address}"
translation_domain=DOMAIN,
translation_key="device_not_found",
translation_placeholders={
"address": address,
"reason": bluetooth.async_address_reachability_diagnostics(
hass,
address.upper(),
BluetoothReachabilityIntent.CONNECTION,
),
},
)
led_ble = LEDBLE(ble_device)
@@ -18,5 +18,10 @@
}
}
}
},
"exceptions": {
"device_not_found": {
"message": "Could not find LED BLE device with address {address}: {reason}"
}
}
}
+4 -1
View File
@@ -105,7 +105,10 @@ class ThinQEntity(CoordinatorEntity[DeviceDataUpdateCoordinator]):
except ThinQAPIException as exc:
if on_fail_method:
on_fail_method()
raise ServiceValidationError(exc.message) from exc
# pylint: disable-next=home-assistant-exception-message-with-translation
raise ServiceValidationError(
exc.message, translation_domain=DOMAIN, translation_key=exc.code
) from exc
except ValueError as exc:
if on_fail_method:
on_fail_method()
+2 -2
View File
@@ -53,8 +53,8 @@
"iot_class": "local_polling",
"loggers": ["aiolifx", "aiolifx_effects", "bitstring"],
"requirements": [
"aiolifx==1.2.2",
"aiolifx==1.2.1",
"aiolifx-effects==0.3.2",
"aiolifx-themes==1.0.4"
"aiolifx-themes==1.0.2"
]
}
+6 -13
View File
@@ -4,7 +4,6 @@ import asyncio
import logging
from typing import TypedDict
import aiohttp
from aiohttp.web import Request
from loqedAPI import loqed
@@ -161,20 +160,14 @@ class LoqedDataCoordinator(DataUpdateCoordinator[StatusMessage]):
_LOGGER.debug("Webhook URL: %s", webhook_url)
try:
webhooks = await self.lock.getWebhooks()
webhooks = await self.lock.getWebhooks()
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
webhook_index = next(
(x["id"] for x in webhooks if x["url"] == webhook_url), None
)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.warning(
"Could not remove webhook from LOQED bridge; the bridge may be offline. Continuing to unload the entry anyway: %s",
err,
)
if webhook_index:
await self.lock.deleteWebhook(webhook_index)
async def async_cloudhook_generate_url(
@@ -97,11 +97,6 @@ class LunatoneConfigFlow(ConfigFlow, domain=DOMAIN):
self._data[CONF_URL] = url
self.context["title_placeholders"] = {
"model": discovery_info.properties["device"],
"name": discovery_info.name.rsplit(" ", maxsplit=1)[0],
}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
@@ -12,7 +12,6 @@
"invalid_url": "Failed to connect. Check the URL and if the device is connected to power",
"missing_device_info": "Failed to read device information. Check the network connection of the device"
},
"flow_title": "{name} ({model})",
"step": {
"discovery_confirm": {
"description": "Do you want to set up the Lunatone device at {url}?"
@@ -556,48 +556,4 @@ DISCOVERY_SCHEMAS = [
featuremap_contains=clusters.Thermostat.Bitmaps.Feature.kOccupancy,
allow_multi=True,
),
# GeneralDiagnostics active fault sensors
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveHardwareFaults",
translation_key="active_hardware_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.GeneralDiagnostics.Attributes.ActiveHardwareFaults,
),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveRadioFaults",
translation_key="active_radio_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.ActiveRadioFaults,),
),
MatterDiscoverySchema(
platform=Platform.BINARY_SENSOR,
entity_description=MatterBinarySensorEntityDescription(
key="GeneralDiagnosticsActiveNetworkFaults",
translation_key="active_network_faults",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=bool,
),
entity_class=MatterBinarySensor,
required_attributes=(
clusters.GeneralDiagnostics.Attributes.ActiveNetworkFaults,
),
),
]
@@ -23,6 +23,7 @@ from matter_ble_proxy import (
)
from homeassistant.components.bluetooth import (
MONOTONIC_TIME,
BluetoothScanningMode,
async_ble_device_from_address,
async_register_callback,
@@ -51,11 +52,18 @@ class HaBluetoothScanSource(BleScanSource):
if self._cancel is not None:
return
# Drop HA's synchronous replay of stale history on register; otherwise a
# rotating peripheral's old addresses each become a parallel connect candidate.
# `MONOTONIC_TIME` is the clock that stamps `service_info.time`.
scan_start = MONOTONIC_TIME()
@callback
def _on_advertisement(
service_info: BluetoothServiceInfoBleak,
_change: object,
) -> None:
if service_info.time < scan_start:
return
try:
callback_fn(_to_advertisement_data(service_info))
except Exception:
+1 -8
View File
@@ -457,14 +457,7 @@ class MatterLight(MatterEntity, LightEntity):
self._transitions_disabled = True
LOGGER.warning(
"Detected a device that has been reported to have firmware issues "
"with light transitions. Transitions will be disabled for this "
"light: %s %s (vendor_id: %s, product_id: %s, hw: %s, sw: %s)",
device_info.vendorName,
device_info.productName,
device_info.vendorID,
device_info.productID,
device_info.hardwareVersionString,
device_info.softwareVersionString,
"with light transitions. Transitions will be disabled for this light"
)
+1 -1
View File
@@ -358,7 +358,7 @@ DISCOVERY_SCHEMAS = [
None if x is None else min(x, 200) / 2
) # Matter range (1-200, capped at 200)
),
ha_to_device=lambda x: round(x * 2), # HA range 0.5-100.0%
ha_to_device=lambda x: round(x * 2), # HA range 0.5100.0%
mode=NumberMode.SLIDER,
),
entity_class=MatterLevelControlNumber,
-130
View File
@@ -27,7 +27,6 @@ from homeassistant.const import (
LIGHT_LUX,
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
Platform,
UnitOfApparentPower,
@@ -138,28 +137,6 @@ RVC_OPERATIONAL_STATE_ERROR_MAP = {
_rvc_err.kNavigationSensorObscured: ("navigation_sensor_obscured"),
}
THREAD_ROUTING_ROLE_MAP = {
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kUnspecified: "unspecified",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kUnassigned: "unassigned",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kSleepyEndDevice: "sleepy_end_device",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kEndDevice: "end_device",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kReed: "reed",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kRouter: "router",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kLeader: "leader",
clusters.ThreadNetworkDiagnostics.Enums.RoutingRoleEnum.kUnknownEnumValue: "unknown",
}
BOOT_REASON_MAP = {
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kUnspecified: "unspecified",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kPowerOnReboot: "power_on_reboot",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kBrownOutReset: "brown_out_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareWatchdogReset: "software_watchdog_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kHardwareWatchdogReset: "hardware_watchdog_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareUpdateCompleted: "software_update_completed",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kSoftwareReset: "software_reset",
clusters.GeneralDiagnostics.Enums.BootReasonEnum.kUnknownEnumValue: None,
}
BOOST_STATE_MAP = {
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kInactive: "inactive",
clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive: "active",
@@ -451,19 +428,6 @@ DISCOVERY_SCHEMAS = [
),
allow_multi=True, # also used for climate entity
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="SoilMoistureSensor",
native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.MOISTURE,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(
clusters.SoilMeasurement.Attributes.SoilMoistureMeasuredValue,
),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
@@ -1611,98 +1575,4 @@ DISCOVERY_SCHEMAS = [
required_attributes=(clusters.DoorLock.Attributes.DoorClosedEvents,),
featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor,
),
# WiFiNetworkDiagnostics cluster sensors
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="WiFiDiagnosticsRssi",
translation_key="wifi_rssi",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
entity_class=MatterSensor,
required_attributes=(clusters.WiFiNetworkDiagnostics.Attributes.Rssi,),
),
# ThreadNetworkDiagnostics cluster sensors
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ThreadDiagnosticsChannel",
translation_key="thread_channel",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
entity_class=MatterSensor,
required_attributes=(clusters.ThreadNetworkDiagnostics.Attributes.Channel,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ThreadDiagnosticsRoutingRole",
translation_key="thread_routing_role",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
options=list(THREAD_ROUTING_ROLE_MAP.values()),
device_to_ha=lambda value: THREAD_ROUTING_ROLE_MAP.get(value, "unknown"),
),
entity_class=MatterSensor,
required_attributes=(clusters.ThreadNetworkDiagnostics.Attributes.RoutingRole,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="ThreadDiagnosticsNetworkName",
translation_key="thread_network_name",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
),
entity_class=MatterSensor,
required_attributes=(clusters.ThreadNetworkDiagnostics.Attributes.NetworkName,),
),
# GeneralDiagnostics cluster sensors
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsRebootCount",
translation_key="reboot_count",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.TOTAL_INCREASING,
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.RebootCount,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsUpTime",
translation_key="uptime",
device_class=SensorDeviceClass.UPTIME,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
device_to_ha=lambda uptime: dt_util.utcnow() - timedelta(seconds=uptime),
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.UpTime,),
),
MatterDiscoverySchema(
platform=Platform.SENSOR,
entity_description=MatterSensorEntityDescription(
key="GeneralDiagnosticsBootReason",
translation_key="boot_reason",
device_class=SensorDeviceClass.ENUM,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
options=[
reason for reason in BOOT_REASON_MAP.values() if reason is not None
],
device_to_ha=BOOT_REASON_MAP.get,
),
entity_class=MatterSensor,
required_attributes=(clusters.GeneralDiagnostics.Attributes.BootReason,),
),
]
@@ -47,15 +47,6 @@
},
"entity": {
"binary_sensor": {
"active_hardware_faults": {
"name": "Hardware faults"
},
"active_network_faults": {
"name": "Network faults"
},
"active_radio_faults": {
"name": "Radio faults"
},
"actuator": {
"name": "Actuator"
},
@@ -417,18 +408,6 @@
"battery_voltage": {
"name": "Battery voltage"
},
"boot_reason": {
"name": "Boot reason",
"state": {
"brown_out_reset": "Brownout reset",
"hardware_watchdog_reset": "Hardware watchdog reset",
"power_on_reboot": "Power-on reboot",
"software_reset": "Software reset",
"software_update_completed": "Software update completed",
"software_watchdog_reset": "Software watchdog reset",
"unspecified": "Unspecified"
}
},
"contamination_state": {
"name": "Contamination state",
"state": {
@@ -597,9 +576,6 @@
"reactive_current": {
"name": "Reactive current"
},
"reboot_count": {
"name": "Reboot count"
},
"rms_current": {
"name": "Effective current"
},
@@ -615,25 +591,6 @@
"tank_volume": {
"name": "Tank volume"
},
"thread_channel": {
"name": "Thread channel"
},
"thread_network_name": {
"name": "Thread network name"
},
"thread_routing_role": {
"name": "Thread routing role",
"state": {
"end_device": "End device",
"leader": "Leader",
"reed": "Router eligible end device",
"router": "Router",
"sleepy_end_device": "Sleepy end device",
"unassigned": "Unassigned",
"unknown": "Unknown",
"unspecified": "Unspecified"
}
},
"tvoc_level": {
"name": "TVOC level",
"state": {
@@ -643,18 +600,12 @@
"medium": "[%key:common::state::medium%]"
}
},
"uptime": {
"name": "Uptime"
},
"valve_position": {
"name": "Valve position"
},
"voltage": {
"name": "Voltage"
},
"wifi_rssi": {
"name": "Wi-Fi RSSI"
},
"window_covering_target_position": {
"name": "Target opening position"
}
@@ -155,7 +155,6 @@ class MediaPlayerDeviceClass(StrEnum):
TV = "tv"
SPEAKER = "speaker"
RECEIVER = "receiver"
PROJECTOR = "projector"
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(MediaPlayerDeviceClass))
@@ -34,12 +34,6 @@
"playing": "mdi:cast-connected"
}
},
"projector": {
"default": "mdi:projector",
"state": {
"off": "mdi:projector-off"
}
},
"receiver": {
"default": "mdi:audio-video",
"state": {
@@ -261,9 +261,6 @@
}
}
},
"projector": {
"name": "Projector"
},
"receiver": {
"name": "Receiver"
},
+1 -1
View File
@@ -798,7 +798,7 @@ class MQTT:
keepalive=self.conf.get(CONF_KEEPALIVE, DEFAULT_KEEPALIVE),
# See:
# https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html
# `clean_start` (bool) - (MQTT v5.0 only) `True`, `False` or
# `clean_start` (bool) (MQTT v5.0 only) `True`, `False` or
# `MQTT_CLEAN_START_FIRST_ONLY`. Sets the MQTT v5.0 clean_start flag
# always, never or on the first successful connect only,
# respectively. MQTT session data (such as outstanding messages and
+2 -2
View File
@@ -1,6 +1,6 @@
"""Helper for Netatmo integration."""
from dataclasses import dataclass, field
from dataclasses import dataclass
from uuid import UUID, uuid4
from pyatmo.modules.device_types import DeviceType as NetatmoDeviceType
@@ -25,4 +25,4 @@ class NetatmoArea:
lon_sw: float
mode: str
show_on_map: bool
uuid: UUID = field(default_factory=uuid4)
uuid: UUID = uuid4()
@@ -1,16 +1,20 @@
"""The openSenseMap integration."""
from opensensemap_api import OpenSenseMap
from opensensemap_api.exceptions import OpenSenseMapError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_STATION_ID
from .coordinator import OpenSenseMapConfigEntry, OpenSenseMapCoordinator
PLATFORMS: list[Platform] = [Platform.AIR_QUALITY]
type OpenSenseMapConfigEntry = ConfigEntry[OpenSenseMap]
async def async_setup_entry(
hass: HomeAssistant, entry: OpenSenseMapConfigEntry
@@ -18,10 +22,14 @@ async def async_setup_entry(
"""Set up openSenseMap from a config entry."""
session = async_get_clientsession(hass)
api = OpenSenseMap(entry.data[CONF_STATION_ID], session)
coordinator = OpenSenseMapCoordinator(hass, entry, api)
await coordinator.async_config_entry_first_refresh()
try:
await api.get_data()
except OpenSenseMapError as err:
raise ConfigEntryNotReady(
f"Unable to fetch data from openSenseMap: {err}"
) from err
entry.runtime_data = coordinator
entry.runtime_data = api
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -1,5 +1,9 @@
"""Support for openSenseMap Air Quality data."""
from datetime import timedelta
from opensensemap_api import OpenSenseMap
from opensensemap_api.exceptions import OpenSenseMapError
import voluptuous as vol
from homeassistant.components.air_quality import (
@@ -16,16 +20,18 @@ from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import OpenSenseMapConfigEntry
from .const import (
CONF_STATION_ID,
DEPRECATED_YAML_BREAKS_IN_VERSION,
DOMAIN,
INTEGRATION_TITLE,
KNOWN_IMPORT_ABORT_REASONS,
LOGGER,
)
from .coordinator import OpenSenseMapConfigEntry, OpenSenseMapCoordinator
SCAN_INTERVAL = timedelta(minutes=10)
PLATFORM_SCHEMA = AIR_QUALITY_PLATFORM_SCHEMA.extend(
{vol.Required(CONF_STATION_ID): cv.string, vol.Optional(CONF_NAME): cv.string}
@@ -101,25 +107,33 @@ async def async_setup_entry(
)
class OpenSenseMapQuality(CoordinatorEntity[OpenSenseMapCoordinator], AirQualityEntity):
class OpenSenseMapQuality(AirQualityEntity):
"""Implementation of an openSenseMap air quality entity."""
_attr_attribution = "Data provided by openSenseMap"
def __init__(
self, coordinator: OpenSenseMapCoordinator, station_id: str, name: str
) -> None:
def __init__(self, api: OpenSenseMap, station_id: str, name: str) -> None:
"""Initialize the air quality entity."""
super().__init__(coordinator)
self._api = api
self._attr_name = name
self._attr_unique_id = station_id
@property
def particulate_matter_2_5(self) -> float | None:
"""Return the particulate matter 2.5 level."""
return self.coordinator.data.pm2_5
return self._api.pm2_5
@property
def particulate_matter_10(self) -> float | None:
"""Return the particulate matter 10 level."""
return self.coordinator.data.pm10
return self._api.pm10
async def async_update(self) -> None:
"""Fetch latest data from the openSenseMap API."""
try:
await self._api.get_data()
except OpenSenseMapError as err:
LOGGER.warning("Unable to fetch data from openSenseMap: %s", err)
self._attr_available = False
else:
self._attr_available = True
@@ -1,58 +0,0 @@
"""Data update coordinator for the openSenseMap integration."""
from dataclasses import dataclass
from datetime import timedelta
from opensensemap_api import OpenSenseMap
from opensensemap_api.exceptions import OpenSenseMapError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
SCAN_INTERVAL = timedelta(minutes=10)
@dataclass(slots=True, frozen=True)
class OpenSenseMapStationData:
"""Immutable measurements for an openSenseMap station."""
pm2_5: float | None
pm10: float | None
type OpenSenseMapConfigEntry = ConfigEntry[OpenSenseMapCoordinator]
class OpenSenseMapCoordinator(DataUpdateCoordinator[OpenSenseMapStationData]):
"""Coordinator to manage data updates for an openSenseMap station."""
config_entry: OpenSenseMapConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: OpenSenseMapConfigEntry,
api: OpenSenseMap,
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
self.api = api
async def _async_update_data(self) -> OpenSenseMapStationData:
"""Fetch latest data from the openSenseMap API."""
try:
await self.api.get_data()
except OpenSenseMapError as err:
raise UpdateFailed(
f"Unable to fetch data from openSenseMap: {err}"
) from err
return OpenSenseMapStationData(pm2_5=self.api.pm2_5, pm10=self.api.pm10)
+7 -17
View File
@@ -93,6 +93,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: OverkizDataConfigEntry)
server=SUPPORTED_SERVERS[entry.data[CONF_HUB]],
)
await _async_migrate_entries(hass, entry)
try:
await client.login()
setup = await client.get_setup()
@@ -194,24 +196,10 @@ async def async_unload_entry(
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(
hass: HomeAssistant, entry: OverkizDataConfigEntry
) -> bool:
"""Migrate old entry."""
if entry.version > 1:
return False
if entry.version == 1 and entry.minor_version < 2:
await _async_migrate_strenum_unique_ids(hass, entry)
hass.config_entries.async_update_entry(entry, minor_version=2)
return True
async def _async_migrate_strenum_unique_ids(
async def _async_migrate_entries(
hass: HomeAssistant, config_entry: OverkizDataConfigEntry
) -> None:
"""Migrate entities to the StrEnum-style unique IDs."""
) -> bool:
"""Migrate old entries to new unique IDs."""
entity_registry = er.async_get(hass)
@callback
@@ -268,6 +256,8 @@ async def _async_migrate_strenum_unique_ids(
await er.async_migrate_entries(hass, config_entry.entry_id, update_unique_id)
return True
def create_local_client(
hass: HomeAssistant, host: str, token: str, verify_ssl: bool
@@ -96,7 +96,7 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
# DomesticHotWaterProduction/WaterHeatingSystem
OverkizBinarySensorDescription(
key=OverkizState.IO_OPERATING_MODE_CAPABILITIES,
name="Energy demand status",
name="Energy Demand Status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: (
cast(dict, state).get(OverkizCommandParam.ENERGY_DEMAND_STATUS) == 1
@@ -40,7 +40,6 @@ class OverkizConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Overkiz (by Somfy)."""
VERSION = 1
MINOR_VERSION = 2
_verify_ssl: bool = True
_api_type: APIType = APIType.CLOUD
@@ -13,7 +13,6 @@ from pyoverkiz.exceptions import (
InvalidEventListenerIdException,
MaintenanceException,
NotAuthenticatedException,
ServiceUnavailableException,
TooManyConcurrentRequestsException,
TooManyRequestsException,
)
@@ -86,8 +85,6 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
raise UpdateFailed("Too many requests, try again later.") from exception
except MaintenanceException as exception:
raise UpdateFailed("Server is down for maintenance.") from exception
except ServiceUnavailableException as exception:
raise UpdateFailed("Server is unavailable.") from exception
except InvalidEventListenerIdException as exception:
raise UpdateFailed(exception) from exception
except (TimeoutError, ClientConnectorError) as exception:
@@ -1,12 +1,6 @@
"""The OVHcloud AI Endpoints integration."""
from openai import (
AsyncOpenAI,
AuthenticationError,
BadRequestError,
OpenAIError,
PermissionDeniedError,
)
from openai import AsyncOpenAI, AuthenticationError, BadRequestError, OpenAIError
from openai.types.chat import ChatCompletionUserMessageParam
from homeassistant.config_entries import ConfigEntry
@@ -58,7 +52,7 @@ async def async_setup_entry(
try:
await _validate_api_key(client)
except (AuthenticationError, PermissionDeniedError) as err:
except AuthenticationError as err:
raise ConfigEntryAuthFailed(err) from err
except OpenAIError as err:
raise ConfigEntryNotReady(err) from err
@@ -1,10 +1,9 @@
"""Config flow for the OVHcloud AI Endpoints integration."""
from collections.abc import Mapping
import logging
from typing import Any
from openai import AsyncOpenAI, AuthenticationError, OpenAIError, PermissionDeniedError
from openai import AsyncOpenAI, AuthenticationError, OpenAIError
import voluptuous as vol
from homeassistant.config_entries import (
@@ -31,8 +30,6 @@ from .const import CONF_PROMPT, DOMAIN, RECOMMENDED_CONVERSATION_OPTIONS
_LOGGER = logging.getLogger(__name__)
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
class OVHcloudAIEndpointsConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for OVHcloud AI Endpoints."""
@@ -58,7 +55,7 @@ class OVHcloudAIEndpointsConfigFlow(ConfigFlow, domain=DOMAIN):
client = _create_client(self.hass, user_input[CONF_API_KEY])
try:
await _validate_api_key(client)
except AuthenticationError, PermissionDeniedError:
except AuthenticationError:
errors["base"] = "invalid_auth"
except OpenAIError:
errors["base"] = "cannot_connect"
@@ -80,39 +77,6 @@ class OVHcloudAIEndpointsConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauth upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
if user_input is not None:
client = _create_client(self.hass, user_input[CONF_API_KEY])
try:
await _validate_api_key(client)
except AuthenticationError, PermissionDeniedError:
errors["base"] = "invalid_auth"
except OpenAIError:
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=STEP_REAUTH_DATA_SCHEMA,
errors=errors,
)
class ConversationFlowHandler(ConfigSubentryFlow):
"""Handle conversation subentry flow."""
@@ -12,8 +12,6 @@ from . import OVHcloudAIEndpointsConfigEntry
from .const import DOMAIN
from .entity import OVHcloudAIEndpointsEntity
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
@@ -1,46 +0,0 @@
"""Diagnostics support for OVHcloud AI Endpoints."""
from typing import TYPE_CHECKING, Any
from openai import __title__, __version__
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_KEY, CONF_PROMPT
from homeassistant.helpers import entity_registry as er
if TYPE_CHECKING:
from homeassistant.core import HomeAssistant
from . import OVHcloudAIEndpointsConfigEntry
TO_REDACT = {CONF_API_KEY, CONF_PROMPT}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: OVHcloudAIEndpointsConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
"client": f"{__title__}=={__version__}",
"title": entry.title,
"entry_id": entry.entry_id,
"entry_version": f"{entry.version}.{entry.minor_version}",
"state": entry.state.value,
"data": async_redact_data(entry.data, TO_REDACT),
"options": async_redact_data(entry.options, TO_REDACT),
"subentries": {
subentry.subentry_id: {
"title": subentry.title,
"subentry_type": subentry.subentry_type,
"data": async_redact_data(subentry.data, TO_REDACT),
}
for subentry in entry.subentries.values()
},
"entities": {
entity_entry.entity_id: entity_entry.extended_dict
for entity_entry in er.async_entries_for_config_entry(
er.async_get(hass), entry.entry_id
)
},
}
@@ -8,6 +8,6 @@
"documentation": "https://www.home-assistant.io/integrations/ovhcloud_ai_endpoints",
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"quality_scale": "bronze",
"requirements": ["openai==2.21.0"]
}
@@ -30,9 +30,7 @@ rules:
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: This integration does not register custom actions.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
@@ -45,13 +43,13 @@ rules:
log-when-unavailable:
status: exempt
comment: the integration only integrates stateless entities
parallel-updates: done
reauthentication-flow: done
parallel-updates: todo
reauthentication-flow: todo
test-coverage: done
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: Service can't be discovered
@@ -1,8 +1,7 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -10,15 +9,6 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::ovhcloud_ai_endpoints::config::step::user::data_description::api_key%]"
},
"description": "The OVHcloud AI Endpoints API key is no longer valid. Please enter a new one."
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
@@ -90,5 +90,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.2.0"]
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.1.0"]
}
+6 -13
View File
@@ -73,26 +73,20 @@ async def _get_endpoint_id(
device_reg = dr.async_get(call.hass)
device_id = call.data[ATTR_DEVICE_ID]
device = device_reg.async_get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
assert device
coordinator = config_entry.runtime_data
endpoint_data = None
for data in coordinator.data.values():
if (
DOMAIN,
f"{config_entry.entry_id}_{data.endpoint.id}",
) in device.identifiers:
return data.endpoint.id
endpoint_data = data
break
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_target",
)
assert endpoint_data
return endpoint_data.endpoint.id
async def _get_container_and_endpoint_ids(
@@ -101,7 +95,6 @@ async def _get_container_and_endpoint_ids(
"""Get config entry, endpoint ID and container ID from the container device ID."""
device_reg = dr.async_get(call.hass)
device = device_reg.async_get(call.data[ATTR_CONTAINER_DEVICE_ID])
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
+2 -2
View File
@@ -3,7 +3,7 @@
from collections.abc import Mapping
from typing import Any
from homeassistant.const import CONF_USERNAME
from homeassistant.const import CONF_TOKEN, CONF_USERNAME
from .const import AUTH_OTHER, CONF_AUTH_METHOD, CONF_REALM, CONF_TOKEN_ID
@@ -21,7 +21,7 @@ def sanitize_config_entry(input_data: Mapping[str, Any]) -> dict[str, Any]:
data[CONF_REALM] = realm
data[CONF_USERNAME] = f"{username}@{realm}"
if CONF_TOKEN_ID in data and "!" in data[CONF_TOKEN_ID]:
if data.get(CONF_TOKEN) and data.get(CONF_TOKEN_ID) and "!" in data[CONF_TOKEN_ID]:
data[CONF_TOKEN_ID] = data[CONF_TOKEN_ID].split("!")[1]
return data
@@ -41,7 +41,6 @@ from .const import (
CONF_VMS,
DEFAULT_PORT,
DEFAULT_REALM,
DEFAULT_TIMEOUT,
DEFAULT_VERIFY_SSL,
DOMAIN,
NODE_ONLINE,
@@ -80,14 +79,14 @@ TOKEN_SCHEMA = vol.Schema(
def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
"""Validate the user input and fetch data (sync, for executor)."""
auth_kwargs = (
{
auth_kwargs = {
"password": data.get(CONF_PASSWORD),
}
if data.get(CONF_TOKEN):
auth_kwargs = {
"token_name": data[CONF_TOKEN_ID],
"token_value": data[CONF_TOKEN_SECRET],
}
if data.get(CONF_TOKEN)
else {"password": data.get(CONF_PASSWORD)}
)
data = sanitize_config_entry(data)
try:
client = ProxmoxAPI(
@@ -95,7 +94,6 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
port=data[CONF_PORT],
user=data[CONF_USERNAME],
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
timeout=DEFAULT_TIMEOUT,
**auth_kwargs,
)
except AuthenticationError as err:
@@ -124,9 +122,6 @@ def _get_nodes_data(data: dict[str, Any]) -> list[dict[str, Any]]:
except requests.exceptions.ConnectionError as err:
raise ProxmoxConnectionError from err
if not nodes:
raise ProxmoxNoNodesFound("No nodes found")
nodes_data: list[dict[str, Any]] = []
for node in nodes:
if node.get("status") != NODE_ONLINE:
@@ -29,7 +29,6 @@ AUTH_METHODS = [AUTH_PAM, AUTH_PVE, AUTH_OTHER]
DEFAULT_PORT = 8006
DEFAULT_REALM = AUTH_PAM
DEFAULT_TIMEOUT = 30
DEFAULT_VERIFY_SSL = True
TYPE_VM = 0
TYPE_CONTAINER = 1
@@ -29,7 +29,6 @@ from .const import (
CONF_NODE,
CONF_TOKEN_ID,
CONF_TOKEN_SECRET,
DEFAULT_TIMEOUT,
DEFAULT_VERIFY_SSL,
DOMAIN,
NODE_ONLINE,
@@ -218,7 +217,6 @@ class ProxmoxCoordinator(DataUpdateCoordinator[dict[str, ProxmoxNodeData]]):
port=data[CONF_PORT],
user=data[CONF_USERNAME],
verify_ssl=data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL),
timeout=DEFAULT_TIMEOUT,
**auth_kwargs,
)
@@ -7,7 +7,7 @@
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": [
"SQLAlchemy==2.0.50",
"SQLAlchemy==2.0.49",
"fnv-hash-fast==2.0.3",
"psutil-home-assistant==0.0.1"
]
+2 -2
View File
@@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, PLATFORMS, RenaultConfigurationKeys
from .const import CONF_LOCALE, DOMAIN, PLATFORMS
from .renault_hub import RenaultHub
from .services import async_setup_services
@@ -28,7 +28,7 @@ async def async_setup_entry(
hass: HomeAssistant, config_entry: RenaultConfigEntry
) -> bool:
"""Load a config entry."""
renault_hub = RenaultHub(hass, config_entry.data[RenaultConfigurationKeys.LOCALE])
renault_hub = RenaultHub(hass, config_entry.data[CONF_LOCALE])
try:
await renault_hub.async_initialise(config_entry)
except NotAuthenticatedException as exc:
+18 -36
View File
@@ -14,20 +14,21 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
)
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from .const import DOMAIN, RenaultConfigurationKeys
from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOCALE, CONF_LOGIN_TOKEN, DOMAIN
from .renault_hub import RenaultHub
_LOGGER = logging.getLogger(__name__)
USER_SCHEMA = vol.Schema(
{
vol.Required(RenaultConfigurationKeys.LOCALE): vol.In(AVAILABLE_LOCALES.keys()),
vol.Required(RenaultConfigurationKeys.USERNAME): str,
vol.Required(RenaultConfigurationKeys.PASSWORD): str,
vol.Required(CONF_LOCALE): vol.In(AVAILABLE_LOCALES.keys()),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
REAUTH_SCHEMA = vol.Schema({vol.Required(RenaultConfigurationKeys.PASSWORD): str})
REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str})
class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -49,14 +50,13 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {}
suggested_values: Mapping[str, Any] | None = None
if user_input:
locale = user_input[RenaultConfigurationKeys.LOCALE]
locale = user_input[CONF_LOCALE]
self.renault_config.update(user_input)
self.renault_config.update(AVAILABLE_LOCALES[locale])
self.renault_hub = RenaultHub(self.hass, locale)
try:
login_success = await self.renault_hub.attempt_login(
user_input[RenaultConfigurationKeys.USERNAME],
user_input[RenaultConfigurationKeys.PASSWORD],
user_input[CONF_USERNAME], user_input[CONF_PASSWORD]
)
except aiohttp.ClientConnectionError, GigyaException:
errors["base"] = "cannot_connect"
@@ -67,9 +67,7 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
if login_success:
if TYPE_CHECKING:
assert self.renault_hub.login_token
self.renault_config[RenaultConfigurationKeys.LOGIN_TOKEN] = (
self.renault_hub.login_token
)
self.renault_config[CONF_LOGIN_TOKEN] = self.renault_hub.login_token
return await self.async_step_kamereon()
errors["base"] = "invalid_credentials"
suggested_values = user_input
@@ -89,9 +87,7 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Select Kamereon account."""
if user_input:
await self.async_set_unique_id(
user_input[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID]
)
await self.async_set_unique_id(user_input[CONF_KAMEREON_ACCOUNT_ID])
if self.source == SOURCE_RECONFIGURE:
self._abort_if_unique_id_mismatch()
self.renault_config.update(user_input)
@@ -104,8 +100,7 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
self.renault_config.update(user_input)
return self.async_create_entry(
title=user_input[RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID],
data=self.renault_config,
title=user_input[CONF_KAMEREON_ACCOUNT_ID], data=self.renault_config
)
accounts = await self.renault_hub.get_account_ids()
@@ -113,17 +108,13 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="kamereon_no_account")
if len(accounts) == 1:
return await self.async_step_kamereon(
user_input={RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID: accounts[0]}
user_input={CONF_KAMEREON_ACCOUNT_ID: accounts[0]}
)
return self.async_show_form(
step_id="kamereon",
data_schema=vol.Schema(
{
vol.Required(RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID): vol.In(
accounts
)
}
{vol.Required(CONF_KAMEREON_ACCOUNT_ID): vol.In(accounts)}
),
)
@@ -141,22 +132,17 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
reauth_entry = self._get_reauth_entry()
if user_input:
# Check credentials
self.renault_hub = RenaultHub(
self.hass, reauth_entry.data[RenaultConfigurationKeys.LOCALE]
)
self.renault_hub = RenaultHub(self.hass, reauth_entry.data[CONF_LOCALE])
if await self.renault_hub.attempt_login(
reauth_entry.data[RenaultConfigurationKeys.USERNAME],
user_input[RenaultConfigurationKeys.PASSWORD],
reauth_entry.data[CONF_USERNAME], user_input[CONF_PASSWORD]
):
if TYPE_CHECKING:
assert self.renault_hub.login_token
return self.async_update_reload_and_abort(
reauth_entry,
data_updates={
RenaultConfigurationKeys.PASSWORD: user_input[
RenaultConfigurationKeys.PASSWORD
],
RenaultConfigurationKeys.LOGIN_TOKEN: self.renault_hub.login_token,
CONF_PASSWORD: user_input[CONF_PASSWORD],
CONF_LOGIN_TOKEN: self.renault_hub.login_token,
},
)
errors = {"base": "invalid_credentials"}
@@ -165,11 +151,7 @@ class RenaultFlowHandler(ConfigFlow, domain=DOMAIN):
step_id="reauth_confirm",
data_schema=REAUTH_SCHEMA,
errors=errors,
description_placeholders={
RenaultConfigurationKeys.USERNAME: reauth_entry.data[
RenaultConfigurationKeys.USERNAME
]
},
description_placeholders={CONF_USERNAME: reauth_entry.data[CONF_USERNAME]},
)
async def async_step_reconfigure(
+3 -12
View File
@@ -1,21 +1,12 @@
"""Constants for the Renault component."""
from typing import Final
from homeassistant.const import Platform
DOMAIN = "renault"
class RenaultConfigurationKeys:
"""Configuration keys."""
KAMEREON_ACCOUNT_ID: Final = "kamereon_account_id"
LOCALE: Final = "locale"
LOGIN_TOKEN: Final = "login_token"
PASSWORD: Final = "password"
USERNAME: Final = "username"
CONF_LOCALE = "locale"
CONF_KAMEREON_ACCOUNT_ID = "kamereon_account_id"
CONF_LOGIN_TOKEN = "login_token"
# normal number of allowed calls per hour to the API
# for a single car and the 7 coordinator, it is a scan every 7mn
@@ -3,18 +3,19 @@
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
from . import RenaultConfigEntry
from .const import RenaultConfigurationKeys
from .const import CONF_KAMEREON_ACCOUNT_ID, CONF_LOGIN_TOKEN
from .renault_vehicle import RenaultVehicleProxy
TO_REDACT = {
RenaultConfigurationKeys.KAMEREON_ACCOUNT_ID,
RenaultConfigurationKeys.LOGIN_TOKEN,
RenaultConfigurationKeys.PASSWORD,
RenaultConfigurationKeys.USERNAME,
CONF_KAMEREON_ACCOUNT_ID,
CONF_LOGIN_TOKEN,
CONF_PASSWORD,
CONF_USERNAME,
"radioCode",
"registrationNumber",
"vin",

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