Compare commits

..

109 Commits

Author SHA1 Message Date
Paulus Schoutsen
516f3295bf 2024.8.3 (#124569) 2024-08-25 16:06:09 +02:00
J. Nick Koston
2d5289e7dd Revert "Exclude aiohappyeyeballs from license check" (#124116) 2024-08-25 13:41:47 +00:00
Paulus Schoutsen
18efd84a35 Bump version to 2024.8.3 2024-08-25 13:26:00 +00:00
Jan Bouwhuis
b34c90b189 Only support remote activity on Alexa if feature is set and at least one feature is in the activity_list (#124567)
Only support remote activity on Alexa if feaure is set and at least one feature is in the activity_list
2024-08-25 13:24:42 +00:00
Mr. Bubbles
a45c1a3914 Fix missing id in Habitica completed todos API response (#124565)
* Fix missing id in completed todos API response

* Copy id only if none

* Update homeassistant/components/habitica/coordinator.py

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>

---------

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
2024-08-25 13:24:42 +00:00
J. Nick Koston
1bdf9d657e Bump yalexs to 8.4.1 (#124553)
changelog: https://github.com/bdraco/yalexs/compare/v8.4.0...v8.4.1
2024-08-25 13:24:41 +00:00
J. Nick Koston
b294a92ad2 Bump yalexs to 8.4.0 (#124520) 2024-08-25 13:24:40 +00:00
J. Nick Koston
2db362ab3d Bump yalexs to 8.3.3 (#124492)
* Bump yalexs to 8.2.0

changelog: https://github.com/bdraco/yalexs/compare/v8.1.4...v8.2.0

* bump to 8.3.1

* bump

* one more bump to ensure we do not hit the ratelimit/shutdown cleanly

* empty commit to restart ci since close/open did not work in flight
2024-08-25 13:24:39 +00:00
Joost Lekkerkerker
5f275a6b9c Don't raise WLED user flow unique_id check (#124481) 2024-08-25 13:22:59 +00:00
Ino Dekker
fa914b2811 Bump aiohue to version 4.7.3 (#124436) 2024-08-25 13:22:58 +00:00
J. Nick Koston
a128e2e4fc Bump yalexs to 8.1.4 (#124425)
changelog: https://github.com/bdraco/yalexs/compare/v8.1.2...v8.1.4
2024-08-25 13:22:57 +00:00
Penny Wood
03c7f2cf5b Add supported features for iZone (#124416)
* Fix for #123462

* Set outside of constructor
2024-08-25 13:22:57 +00:00
Angel Nunez Mencias
102528e5d3 update ttn_client - fix crash with SenseCAP devices (#124370)
update ttn_client
2024-08-25 13:22:56 +00:00
Pete Sage
8f4af4f7c2 Fix Spotify Media Browsing fails for new config entries (#124368)
* initial commit

* tests

* tests

* update tests

* update tests

* update tests
2024-08-25 13:22:55 +00:00
karwosts
667af10017 Add missing strings for riemann options flow (#124317) 2024-08-25 13:22:54 +00:00
G Johansson
e5a64a1e0a Bump python-holidays to 0.55 (#124314) 2024-08-25 13:22:54 +00:00
G Johansson
236fa8e238 Bump python-holidays to 0.54 (#124170) 2024-08-25 13:22:53 +00:00
J. Nick Koston
70a58a0bb0 Bump yalexs to 8.1.2 (#124303) 2024-08-25 13:20:54 +00:00
Joost Lekkerkerker
769c7f1ea3 Don't abort airgradient user flow if flow in progress (#124300) 2024-08-25 13:20:53 +00:00
Marcel van der Veldt
5a8045d1fb Prevent KeyError when Matter device sends invalid value for StartUpOnOff (#124280) 2024-08-25 13:20:52 +00:00
Allen Porter
5a73b636e3 Bump python-roborock to 2.6.0 (#124268) 2024-08-25 13:20:51 +00:00
Matthias Alphart
524e09b45e Update xknx to 3.1.1 (#124257) 2024-08-25 13:20:51 +00:00
J. Nick Koston
1f46670266 Bump aiohttp to 3.10.5 (#124254) 2024-08-25 13:20:50 +00:00
Erik Montnemery
a857f603c8 Bump pyhomeworks to 1.1.2 (#124199) 2024-08-25 13:20:49 +00:00
J. Nick Koston
b7d8f3d005 Fix shelly available check when device is not initialized (#124182)
* Fix shelly available check when device is not initialized

available needs to check for device.initialized or if the device
is sleepy as calls to status will raise NotInitialized which results
in many unretrieved exceptions while writing state

fixes
```
2024-08-18 09:33:03.757 ERROR (MainThread) [homeassistant] Error doing job: Task exception was never retrieved (None)
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 258, in _handle_refresh_interval
    await self._async_refresh(log_failures=True, scheduled=True)
  File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 453, in _async_refresh
    self.async_update_listeners()
  File "/usr/src/homeassistant/homeassistant/helpers/update_coordinator.py", line 168, in async_update_listeners
    update_callback()
  File "/config/custom_components/shelly/entity.py", line 374, in _update_callback
    self.async_write_ha_state()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1005, in async_write_ha_state
    self._async_write_ha_state()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1130, in _async_write_ha_state
    self.__async_calculate_state()
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1067, in __async_calculate_state
    state = self._stringify_state(available)
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity.py", line 1011, in _stringify_state
    if (state := self.state) is None:
                 ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/binary_sensor/__init__.py", line 293, in state
    if (is_on := self.is_on) is None:
                 ^^^^^^^^^^
  File "/config/custom_components/shelly/binary_sensor.py", line 331, in is_on
    return bool(self.attribute_value)
                ^^^^^^^^^^^^^^^^^^^^
  File "/config/custom_components/shelly/entity.py", line 545, in attribute_value
    self._last_value = self.sub_status
                       ^^^^^^^^^^^^^^^
  File "/config/custom_components/shelly/entity.py", line 534, in sub_status
    return self.status[self.entity_description.sub_key]
           ^^^^^^^^^^^
  File "/config/custom_components/shelly/entity.py", line 364, in status
    return cast(dict, self.coordinator.device.status[self.key])
                      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/aioshelly/rpc_device/device.py", line 390, in status
    raise NotInitialized
aioshelly.exceptions.NotInitialized
```

* tweak

* cover

* fix

* cover

* fixes
2024-08-25 13:19:57 +00:00
Shay Levy
129035967b Shelly RPC - do not stop BLE scanner if a sleeping device (#124147) 2024-08-25 13:17:46 +00:00
Christopher Maio
45b44f8a59 Update Matter light transition blocklist to include GE Cync Undercabinet Lights (#124138) 2024-08-25 13:17:45 +00:00
J. Nick Koston
e80dc52175 Bump aiohttp to 3.10.4 (#124137)
changelog: https://github.com/aio-libs/aiohttp/compare/v3.10.3...v3.10.4
2024-08-25 13:17:45 +00:00
MarkGodwin
22bb3e5477 Bump tplink-omada-api to 1.4.2 (#124136)
Fix for bad pre-registered clients
2024-08-25 13:17:44 +00:00
Allen Porter
f89e8e6ceb Bump nest to 4.0.7 to increase subscriber deadline (#124131)
Bump nest to 4.0.7
2024-08-25 13:17:43 +00:00
J. Nick Koston
157a61845b Bump aiohomekit to 3.2.3 (#124115) 2024-08-25 13:17:43 +00:00
Artem Draft
0fcdc3c200 Bump pybravia to 0.3.4 (#124113) 2024-08-25 13:17:42 +00:00
cdnninja
d1f09ecd0c Add Alt Core300s model to vesync integration (#124091) 2024-08-25 13:17:41 +00:00
J. Nick Koston
3484ab3c0c Bump aioshelly to 11.2.4 (#124080) 2024-08-25 13:17:40 +00:00
J. Nick Koston
80df582ebd Bump yalexs to 8.0.2 (#123817) 2024-08-25 13:17:39 +00:00
J. Nick Koston
dc967e2ef2 Bump yalexs to 6.5.0 (#123739) 2024-08-25 13:17:39 +00:00
Daniel Rozycki
e2c1a38d87 Skip NextBus update if integration is still loading (#123564)
* Skip NextBus update if integration is still loading

Fixes a race between the loading thread and
update thread leading to an unrecoverable error

* Use async_at_started

* Use local copy of _route_stops to avoid NextBus race condition

* Update homeassistant/components/nextbus/coordinator.py

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-08-25 13:17:38 +00:00
Franck Nijhof
94516de724 2024.8.2 (#124069) 2024-08-16 18:43:41 +02:00
Joost Lekkerkerker
a2027fc78c Exclude aiohappyeyeballs from license check (#124041) 2024-08-16 18:13:33 +02:00
Franck Nijhof
be5577c2f9 Bump version to 2024.8.2 2024-08-16 18:02:52 +02:00
Joost Lekkerkerker
93dc08a05f Bump aiomealie to 0.8.1 (#124047) 2024-08-16 18:02:41 +02:00
Matthias Alphart
def2ace4ec Fix loading KNX integration actions when not using YAML (#124027)
* Fix loading KNX integration services when not using YAML

* remove unnecessary comment

* Remove unreachable test
2024-08-16 18:02:38 +02:00
J. Nick Koston
4f0261d739 Bump bluetooth-adapters to 0.19.4 (#124018)
Fixes a call to enumerate USB devices that did blocking
I/O
2024-08-16 18:02:35 +02:00
Brett Adams
6103811de8 Fix rear trunk logic in Tessie (#124011)
Allow open to be anything not zero
2024-08-16 18:02:32 +02:00
Robert Svensson
fd904c65a7 Bump aiounifi to v80 (#124004) 2024-08-16 18:02:29 +02:00
Joost Lekkerkerker
04bf8482b2 Re-enable concord232 (#124000) 2024-08-16 18:02:26 +02:00
Sid
f5fd5e0457 Bump openwebifpy to 4.2.7 (#123995)
* Bump openwebifpy to 4.2.6

* Bump openwebifpy to 4.2.7

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-08-16 18:02:23 +02:00
J. Nick Koston
0de89b42aa Ensure event entities are allowed for linked homekit config via YAML (#123994) 2024-08-16 18:02:20 +02:00
Erik Montnemery
e8914552b1 Bump pyhomeworks to 1.1.1 (#123981) 2024-08-16 18:02:17 +02:00
Glenn Waters
bfd302109e Environment Canada weather format fix (#123960)
* Add missing isoformat.

* Move fixture loading to common conftest.py

* Add deepcopy.
2024-08-16 18:02:14 +02:00
Andre Lengwenus
796ad47dd0 Bump pypck to 0.7.20 (#123948) 2024-08-16 18:02:11 +02:00
IceBotYT
e9915463a9 Bump LaCrosse View to 1.0.2, fixes blocking call (#123935) 2024-08-16 18:02:07 +02:00
Michael
59aecda8cf Fix PI-Hole update entity when no update available (#123930)
show installed version when no update available
2024-08-16 17:58:24 +02:00
J. Nick Koston
7d00ccbbbc Bump pylutron_caseta to 0.21.1 (#123924) 2024-08-16 17:58:21 +02:00
Álvaro Fernández Rojas
55a911120c Handle timeouts on Airzone DHCP config flow (#123869)
airzone: config_flow: dhcp: catch timeout exception

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-08-16 17:58:18 +02:00
Michael
80abf90c87 Fix translation for integration not found repair issue (#123868)
* correct setp id in strings

* add issue_ignored string
2024-08-16 17:58:15 +02:00
Robert Resch
8539591307 Fix blocking I/O of SSLContext.load_default_certs in Ecovacs (#123856) 2024-08-16 17:58:12 +02:00
Michael
6234deeee1 Bump py-synologydsm-api to 2.4.5 (#123815)
bump py-synologydsm-api to 2.4.5
2024-08-16 17:57:59 +02:00
Louis Christ
81fabb1bfa Fix status update loop in bluesound integration (#123790)
* Fix retry loop for status update

* Use 'available' instead of _is_online

* Fix tests
2024-08-16 17:56:23 +02:00
Matthias Alphart
ff4e5859cf Fix KNX UI Light color temperature DPT (#123778) 2024-08-16 17:13:31 +02:00
Matthias Alphart
f2e42eafc7 Update xknx to 3.1.0 and fix climate read only mode (#123776) 2024-08-16 17:13:28 +02:00
Allen Porter
63f28ae2fe Bump python-nest-sdm to 4.0.6 (#123762) 2024-08-16 17:13:25 +02:00
Ian
5b6c6141c5 Bump py-nextbusnext to 2.0.4 (#123750) 2024-08-16 17:13:22 +02:00
Michael
396ef7a642 Fix error message in html5 (#123749) 2024-08-16 17:13:19 +02:00
Franck Nijhof
17f59a5665 Update wled to 0.20.2 (#123746) 2024-08-16 17:13:16 +02:00
David F. Mulcahey
10846dc97b Bump ZHA lib to 0.0.31 (#123743) 2024-08-16 17:13:13 +02:00
Álvaro Fernández Rojas
17bb00727d Update aioqsw to v0.4.1 (#123721) 2024-08-16 17:13:10 +02:00
Álvaro Fernández Rojas
bc021dbbc6 Update aioairzone-cloud to v0.6.2 (#123719) 2024-08-16 17:13:06 +02:00
Álvaro Fernández Rojas
e3cb9c0844 Update AEMET-OpenData to v0.5.4 (#123716) 2024-08-16 17:13:03 +02:00
David Knowles
050e2c9404 Bump pyschlage to 2024.8.0 (#123714) 2024-08-16 17:13:00 +02:00
Cyrill Raccaud
5ea447ba48 Fix startup block from Swiss public transport (#123704) 2024-08-16 17:12:57 +02:00
J. Nick Koston
a23b063922 Bump aiohomekit to 3.2.2 (#123669) 2024-08-16 17:12:53 +02:00
Aidan Timson
c269d57259 System Bridge package updates (#123657) 2024-08-16 17:12:50 +02:00
kingy444
d512f327c5 Bump pydaikin to 2.13.4 (#123623)
* bump pydaikin to 2.13.3

* bump pydaikin to 2.13.4
2024-08-16 17:12:46 +02:00
Maciej Bieniek
9bf8c5a54b Bump aioshelly to version 11.2.0 (#123602)
Bump aioshelly to version 11.2.0
2024-08-16 17:12:43 +02:00
J. Nick Koston
725e2f16f5 Ensure HomeKit connection is kept alive for devices that timeout too quickly (#123601) 2024-08-16 17:12:21 +02:00
G Johansson
d98d0cdad0 Change WoL to be secondary on device info (#123591) 2024-08-16 17:07:24 +02:00
Noah Husby
e2f4aa893f Fix secondary russound controller discovery failure (#123590) 2024-08-16 17:07:21 +02:00
Matthias Alphart
6b81fa89d3 Update knx-frontend to 2024.8.9.225351 (#123557) 2024-08-16 17:07:18 +02:00
J. Nick Koston
c886587915 Bump aiohttp to 3.10.3 (#123549) 2024-08-16 17:07:15 +02:00
Phill (pssc)
059d3eed98 Handle Yamaha ValueError (#123547)
* fix yamaha remove info logging

* ruff

* fix yamnaha supress rxv.find UnicodeDecodeError

* fix formatting

* make more realistic

* make more realistic and use parms

* add value error after more feedback

* ruff format

* Update homeassistant/components/yamaha/media_player.py

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

* remove unused method

* add more debugging

* Increase discovery timeout add more debug allow config to overrite dicovery for name

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-08-16 17:07:12 +02:00
Erik Montnemery
f9ae2b4453 Drop violating rows before adding foreign constraints in DB schema 44 migration (#123454)
* Drop violating rows before adding foreign constraints

* Don't delete rows with null-references

* Only delete rows when integrityerror is caught

* Move restore of dropped foreign key constraints to a separate migration step

* Use aliases for tables

* Update homeassistant/components/recorder/migration.py

* Update test

* Don't use alias for table we're deleting from, improve test

* Fix MySQL

* Update instead of deleting in case of self references

* Improve log messages

* Batch updates

* Add workaround for unsupported LIMIT in PostgreSQL

* Simplify

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-08-16 17:07:09 +02:00
ilan
742c7ba23f Fix Madvr sensor values on startup (#122479)
* fix: add startup values

* fix: update snap

* fix: use native value to show None
2024-08-16 17:07:06 +02:00
wittypluck
e7ae5c5c24 Avoid Exception on Glances missing key (#114628)
* Handle case of sensors removed server side

* Update available state on value update

* Set uptime to None if key is missing

* Replace _attr_available by _data_valid
2024-08-16 17:07:02 +02:00
Franck Nijhof
ae4fc9504a 2024.8.1 (#123544) 2024-08-10 19:32:02 +02:00
Franck Nijhof
2ef337ec2e Bump version to 2024.8.1 2024-08-10 18:41:57 +02:00
cnico
723b7bd532 Upgrade chacon_dio_api to version 1.2.0 (#123528)
Upgrade api version 1.2.0 with the first user feedback improvement
2024-08-10 18:41:39 +02:00
Joost Lekkerkerker
4fdb11b0d8 Bump AirGradient to 0.8.0 (#123527) 2024-08-10 18:41:36 +02:00
Matt Way
fe2e6c37f4 Bump pydaikin to 2.13.2 (#123519) 2024-08-10 18:41:32 +02:00
Michael
4a75c55a8f Fix cleanup of old orphan device entries in AVM Fritz!Tools (#123516)
fix cleanup of old orphan device entries
2024-08-10 18:41:29 +02:00
Duco Sebel
dfb59469cf Bumb python-homewizard-energy to 6.2.0 (#123514) 2024-08-10 18:41:26 +02:00
David F. Mulcahey
bdb2e1e2e9 Bump zha lib to 0.0.30 (#123499) 2024-08-10 18:41:22 +02:00
Franck Nijhof
c4f6f1e3d8 Update frontend to 20240809.0 (#123485) 2024-08-10 18:41:19 +02:00
Louis Christ
fb3eae54ea Fix startup blocked by bluesound integration (#123483) 2024-08-10 18:41:16 +02:00
Jake Martin
d3f8fce788 Bump monzopy to 1.3.2 (#123480) 2024-08-10 18:41:13 +02:00
Steve Easley
44e58a8c87 Bump pyjvcprojector to 1.0.12 to fix blocking call (#123473) 2024-08-10 18:41:09 +02:00
puddly
3d3879b0db Bump ZHA library to 0.0.29 (#123464)
* Bump zha to 0.0.29

* Pass the Core timezone to ZHA

* Add a unit test
2024-08-10 18:41:06 +02:00
Franck Nijhof
a8b1eb34f3 Support action YAML syntax in old-style notify groups (#123457) 2024-08-10 18:41:03 +02:00
Matrix
fd77058def Bump YoLink API to 0.4.7 (#123441) 2024-08-10 18:41:00 +02:00
Brett Adams
b147ca6c5b Add missing logger to Tessie (#123413) 2024-08-10 18:40:57 +02:00
dupondje
670c4cacfa Also migrate dsmr entries for devices with correct serial (#123407)
dsmr: also migrate entries for devices with correct serial

When the dsmr code could not find the serial_nr for the gas meter,
it creates the gas meter device with the entry_id as identifier.

But when there is a correct serial_nr, it will use that as identifier
for the dsmr gas device.

Now the migration code did not take this into account, so migration to
the new name failed since it didn't look for the device with correct
serial_nr.

This commit fixes this and adds a test for this.
2024-08-10 18:40:53 +02:00
J. Nick Koston
1ed0a89303 Bump aiohttp to 3.10.2 (#123394) 2024-08-10 18:40:50 +02:00
J. Nick Koston
ab0597da7b Ensure legacy event foreign key is removed from the states table when a previous rebuild failed (#123388)
* Ensure legacy event foreign key is removed from the states table

If the system ran out of disk space removing the FK, it would
fail. #121938 fixed that to try again, however that PR was made
ineffective by #122069 since it will never reach the check.

To solve this, the migration version is incremented to 2, and
the migration is no longer marked as done unless the rebuild
/fk removal is successful.

* fix logic for mysql

* fix test

* asserts

* coverage

* coverage

* narrow test

* fixes

* split tests

* should have skipped

* fixture must be used
2024-08-10 18:40:47 +02:00
Erik Montnemery
a3db6bc8fa Revert "Fix blocking I/O while validating config schema" (#123377) 2024-08-10 18:40:44 +02:00
Noah Husby
9bfc8f6e27 Bump aiorussound to 2.2.2 (#123319) 2024-08-10 18:40:41 +02:00
J. Nick Koston
6fddef2dc5 Fix doorbird with externally added events (#123313) 2024-08-10 18:40:38 +02:00
fustom
ec08a85aa0 Fix limit and order property for transmission integration (#123305) 2024-08-10 18:40:35 +02:00
Evgeny
de7af575c5 Bump OpenWeatherMap to 0.1.1 (#120178)
* add owm modes

* fix tests

* fix modes

* remove sensors

* Update homeassistant/components/openweathermap/sensor.py

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

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-08-10 18:40:32 +02:00
Tom Brien
d3831bae4e Add support for v3 Coinbase API (#116345)
* Add support for v3 Coinbase API

* Add deps

* Move tests
2024-08-10 18:40:28 +02:00
1446 changed files with 42614 additions and 39634 deletions

View File

@@ -146,7 +146,6 @@ requirements: &requirements
- homeassistant/package_constraints.txt
- requirements*.txt
- pyproject.toml
- script/licenses.py
any:
- *base_platforms

View File

@@ -69,7 +69,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: translations
path: translations.tar.gz
@@ -197,7 +197,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.08.2
uses: home-assistant/builder@2024.03.5
with:
args: |
$BUILD_ARGS \
@@ -263,7 +263,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build base image
uses: home-assistant/builder@2024.08.2
uses: home-assistant/builder@2024.03.5
with:
args: |
$BUILD_ARGS \
@@ -323,7 +323,7 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Install Cosign
uses: sigstore/cosign-installer@v3.6.0
uses: sigstore/cosign-installer@v3.5.0
with:
cosign-release: "v2.2.3"

View File

@@ -31,16 +31,12 @@ on:
description: "Only run mypy"
default: false
type: boolean
audit-licenses-only:
description: "Only run audit licenses"
default: false
type: boolean
env:
CACHE_VERSION: 10
CACHE_VERSION: 9
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 8
HA_SHORT_VERSION: "2024.9"
HA_SHORT_VERSION: "2024.8"
DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12']"
# 10.3 is the oldest supported version
@@ -226,7 +222,6 @@ jobs:
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs:
- info
steps:
@@ -348,7 +343,6 @@ jobs:
pre-commit run --hook-stage manual ruff --all-files --show-diff-on-failure
env:
RUFF_OUTPUT_FORMAT: github
lint-other:
name: Check other linters
runs-on: ubuntu-24.04
@@ -514,7 +508,8 @@ jobs:
uv pip install -U "pip>=21.3.1" setuptools wheel
uv pip install -r requirements.txt
python -m script.gen_requirements_all ci
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
uv pip install -r requirements_all_pytest.txt
uv pip install -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat
hassfest:
@@ -523,7 +518,6 @@ jobs:
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs:
- info
- base
@@ -562,7 +556,6 @@ jobs:
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
needs:
- info
- base
@@ -596,10 +589,7 @@ jobs:
- info
- base
if: |
(github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
|| github.event.inputs.audit-licenses-only == 'true')
&& needs.info.outputs.requirements == 'true'
needs.info.outputs.requirements == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
@@ -623,7 +613,7 @@ jobs:
. venv/bin/activate
pip-licenses --format=json --output-file=licenses.json
- name: Upload licenses
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: licenses
path: licenses.json
@@ -638,7 +628,6 @@ jobs:
timeout-minutes: 20
if: |
github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.pylint-only == 'true'
needs:
- info
@@ -683,9 +672,7 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 20
if: |
(github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.pylint-only == 'true')
(github.event.inputs.mypy-only != 'true' || github.event.inputs.pylint-only == 'true')
&& (needs.info.outputs.tests_glob || needs.info.outputs.test_full_suite == 'true')
needs:
- info
@@ -716,21 +703,20 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint tests
pylint --ignore-missing-annotations=y tests
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
run: |
. venv/bin/activate
python --version
pylint tests/components/${{ needs.info.outputs.tests_glob }}
pylint --ignore-missing-annotations=y tests/components/${{ needs.info.outputs.tests_glob }}
mypy:
name: Check mypy
runs-on: ubuntu-24.04
if: |
github.event.inputs.pylint-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
|| github.event.inputs.mypy-only == 'true'
needs:
- info
@@ -795,7 +781,6 @@ jobs:
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.test_full_suite == 'true'
needs:
- info
@@ -833,7 +818,7 @@ jobs:
. venv/bin/activate
python -m script.split_tests ${{ needs.info.outputs.test_group_count }} tests
- name: Upload pytest_buckets
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: pytest_buckets
path: pytest_buckets.txt
@@ -846,7 +831,6 @@ jobs:
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.test_full_suite == 'true'
needs:
- info
@@ -934,14 +918,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-full.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml
@@ -967,7 +951,6 @@ jobs:
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.mariadb_groups != '[]'
needs:
- info
@@ -1060,7 +1043,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${mariadb}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1068,7 +1051,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.mariadb }}
@@ -1093,7 +1076,6 @@ jobs:
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.postgresql_groups != '[]'
needs:
- info
@@ -1187,7 +1169,7 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${postgresql}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1195,7 +1177,7 @@ jobs:
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: coverage-${{ matrix.python-version }}-${{
steps.pytest-partial.outputs.postgresql }}
@@ -1238,7 +1220,6 @@ jobs:
&& github.event.inputs.lint-only != 'true'
&& github.event.inputs.pylint-only != 'true'
&& github.event.inputs.mypy-only != 'true'
&& github.event.inputs.audit-licenses-only != 'true'
&& needs.info.outputs.tests_glob
&& needs.info.outputs.test_full_suite == 'false'
needs:
@@ -1329,14 +1310,14 @@ jobs:
2>&1 | tee pytest-${{ matrix.python-version }}-${{ matrix.group }}.txt
- name: Upload pytest output
if: success() || failure() && steps.pytest-partial.conclusion == 'failure'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: pytest-${{ github.run_number }}-${{ matrix.python-version }}-${{ matrix.group }}
path: pytest-*.txt
overwrite: true
- name: Upload coverage artifact
if: needs.info.outputs.skip_coverage != 'true'
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: coverage-${{ matrix.python-version }}-${{ matrix.group }}
path: coverage.xml

View File

@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.1.7
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.26.4
uses: github/codeql-action/init@v3.25.15
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.26.4
uses: github/codeql-action/analyze@v3.25.15
with:
category: "/language:python"

View File

@@ -82,14 +82,14 @@ jobs:
) > .env_file
- name: Upload env_file
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: env_file
path: ./.env_file
overwrite: true
- name: Upload requirements_diff
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: requirements_diff
path: ./requirements_diff.txt
@@ -101,7 +101,7 @@ jobs:
python -m script.gen_requirements_all ci
- name: Upload requirements_all_wheels
uses: actions/upload-artifact@v4.3.6
uses: actions/upload-artifact@v4.3.4
with:
name: requirements_all_wheels
path: ./requirements_all_wheels_*.txt
@@ -211,7 +211,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_old-cython.txt"
@@ -226,7 +226,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtaa"
@@ -240,7 +240,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtab"
@@ -254,7 +254,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true
apk: "bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev;yaml-dev;openblas-dev;fftw-dev;lapack-dev;gfortran;blas-dev;eigen-dev;freetype-dev;glew-dev;harfbuzz-dev;hdf5-dev;libdc1394-dev;libtbb-dev;mesa-dev;openexr-dev;openjpeg-dev;uchardet-dev;nasm"
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic;pymicro-vad
skip-binary: aiohttp;charset-normalizer;grpcio;SQLAlchemy;protobuf;pydantic
constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt"
requirements: "requirements_all.txtac"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.2
rev: v0.5.5
hooks:
- id: ruff
args:
@@ -12,7 +12,7 @@ repos:
hooks:
- id: codespell
args:
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn
- --ignore-words-list=astroid,checkin,currenty,hass,iif,incomfort,lookin,nam,NotIn,pres,ser,ue
- --skip="./.*,*.csv,*.json,*.ambr"
- --quiet-level=2
exclude_types: [csv, json, html]

View File

@@ -95,6 +95,8 @@ homeassistant.components.aruba.*
homeassistant.components.arwn.*
homeassistant.components.aseko_pool_live.*
homeassistant.components.assist_pipeline.*
homeassistant.components.asterisk_cdr.*
homeassistant.components.asterisk_mbox.*
homeassistant.components.asuswrt.*
homeassistant.components.autarco.*
homeassistant.components.auth.*
@@ -196,9 +198,7 @@ homeassistant.components.fritzbox.*
homeassistant.components.fritzbox_callmonitor.*
homeassistant.components.fronius.*
homeassistant.components.frontend.*
homeassistant.components.fujitsu_fglair.*
homeassistant.components.fully_kiosk.*
homeassistant.components.fyta.*
homeassistant.components.generic_hygrostat.*
homeassistant.components.generic_thermostat.*
homeassistant.components.geo_location.*
@@ -295,7 +295,6 @@ homeassistant.components.lookin.*
homeassistant.components.luftdaten.*
homeassistant.components.madvr.*
homeassistant.components.mailbox.*
homeassistant.components.manual.*
homeassistant.components.map.*
homeassistant.components.mastodon.*
homeassistant.components.matrix.*

View File

@@ -108,8 +108,6 @@ build.json @home-assistant/supervisor
/tests/components/anova/ @Lash-L
/homeassistant/components/anthemav/ @hyralex
/tests/components/anthemav/ @hyralex
/homeassistant/components/anthropic/ @Shulyaka
/tests/components/anthropic/ @Shulyaka
/homeassistant/components/aosmith/ @bdr99
/tests/components/aosmith/ @bdr99
/homeassistant/components/apache_kafka/ @bachya
@@ -349,8 +347,8 @@ build.json @home-assistant/supervisor
/tests/components/dremel_3d_printer/ @tkdrob
/homeassistant/components/drop_connect/ @ChandlerSystems @pfrazer
/tests/components/drop_connect/ @ChandlerSystems @pfrazer
/homeassistant/components/dsmr/ @Robbie1221
/tests/components/dsmr/ @Robbie1221
/homeassistant/components/dsmr/ @Robbie1221 @frenck
/tests/components/dsmr/ @Robbie1221 @frenck
/homeassistant/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/tests/components/dsmr_reader/ @sorted-bits @glodenox @erwindouna
/homeassistant/components/duotecno/ @cereal2nd
@@ -433,7 +431,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/evil_genius_labs/ @balloob
/tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb
/tests/components/evohome/ @zxdavb
/homeassistant/components/ezviz/ @RenierM26 @baqs
/tests/components/ezviz/ @RenierM26 @baqs
/homeassistant/components/faa_delays/ @ntilley905
@@ -499,8 +496,6 @@ build.json @home-assistant/supervisor
/tests/components/frontend/ @home-assistant/frontend
/homeassistant/components/frontier_silicon/ @wlcrs
/tests/components/frontier_silicon/ @wlcrs
/homeassistant/components/fujitsu_fglair/ @crevetor
/tests/components/fujitsu_fglair/ @crevetor
/homeassistant/components/fully_kiosk/ @cgarwood
/tests/components/fully_kiosk/ @cgarwood
/homeassistant/components/fyta/ @dontinelli
@@ -828,6 +823,8 @@ build.json @home-assistant/supervisor
/tests/components/logbook/ @home-assistant/core
/homeassistant/components/logger/ @home-assistant/core
/tests/components/logger/ @home-assistant/core
/homeassistant/components/logi_circle/ @evanjd
/tests/components/logi_circle/ @evanjd
/homeassistant/components/london_underground/ @jpbede
/tests/components/london_underground/ @jpbede
/homeassistant/components/lookin/ @ANMalko @bdraco
@@ -970,8 +967,6 @@ build.json @home-assistant/supervisor
/tests/components/nfandroidtv/ @tkdrob
/homeassistant/components/nibe_heatpump/ @elupus
/tests/components/nibe_heatpump/ @elupus
/homeassistant/components/nice_go/ @IceBotYT
/tests/components/nice_go/ @IceBotYT
/homeassistant/components/nightscout/ @marciogranzotto
/tests/components/nightscout/ @marciogranzotto
/homeassistant/components/nilu/ @hfurubotten
@@ -1329,8 +1324,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/smarty/ @z0mbieprocess
/homeassistant/components/smhi/ @gjohansson-ST
/tests/components/smhi/ @gjohansson-ST
/homeassistant/components/smlight/ @tl-sl
/tests/components/smlight/ @tl-sl
/homeassistant/components/sms/ @ocalvo
/tests/components/sms/ @ocalvo
/homeassistant/components/snapcast/ @luar123

View File

@@ -8,8 +8,6 @@ import glob
from http.client import HTTPConnection
import importlib
import os
from pathlib import Path
from ssl import SSLContext
import sys
import threading
import time
@@ -145,78 +143,6 @@ _BLOCKING_CALLS: tuple[BlockingCall, ...] = (
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.load_default_certs,
object=SSLContext,
function="load_default_certs",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.load_verify_locations,
object=SSLContext,
function="load_verify_locations",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=SSLContext.load_cert_chain,
object=SSLContext,
function="load_cert_chain",
check_allowed=None,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.open,
object=Path,
function="open",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.read_text,
object=Path,
function="read_text",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.read_bytes,
object=Path,
function="read_bytes",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.write_text,
object=Path,
function="write_text",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
BlockingCall(
original_func=Path.write_bytes,
object=Path,
function="write_bytes",
check_allowed=_check_file_allowed,
strict=False,
strict_core=False,
skip_for_tests=True,
),
)

View File

@@ -586,10 +586,10 @@ async def async_enable_logging(
logging.getLogger("aiohttp.access").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
sys.excepthook = lambda *args: logging.getLogger().exception(
sys.excepthook = lambda *args: logging.getLogger(None).exception(
"Uncaught exception", exc_info=args
)
threading.excepthook = lambda args: logging.getLogger().exception(
threading.excepthook = lambda args: logging.getLogger(None).exception(
"Uncaught thread exception",
exc_info=( # type: ignore[arg-type]
args.exc_type,
@@ -616,9 +616,10 @@ async def async_enable_logging(
_create_log_file, err_log_path, log_rotate_days
)
err_handler.setLevel(logging.INFO if verbose else logging.WARNING)
err_handler.setFormatter(logging.Formatter(fmt, datefmt=FORMAT_DATETIME))
logger = logging.getLogger()
logger = logging.getLogger("")
logger.addHandler(err_handler)
logger.setLevel(logging.INFO if verbose else logging.WARNING)

View File

@@ -0,0 +1,5 @@
{
"domain": "asterisk",
"name": "Asterisk",
"integrations": ["asterisk_cdr", "asterisk_mbox"]
}

View File

@@ -1,5 +0,0 @@
{
"domain": "fujitsu",
"name": "Fujitsu",
"integrations": ["fujitsu_anywair", "fujitsu_fglair"]
}

View File

@@ -81,7 +81,7 @@ class AcerSwitch(SwitchEntity):
write_timeout: int,
) -> None:
"""Init of the Acer projector."""
self.serial = serial.Serial(
self.ser = serial.Serial(
port=serial_port, timeout=timeout, write_timeout=write_timeout
)
self._serial_port = serial_port
@@ -99,16 +99,16 @@ class AcerSwitch(SwitchEntity):
# was disconnected during runtime.
# This way the projector can be reconnected and will still work
try:
if not self.serial.is_open:
self.serial.open()
self.serial.write(msg.encode("utf-8"))
if not self.ser.is_open:
self.ser.open()
self.ser.write(msg.encode("utf-8"))
# Size is an experience value there is no real limit.
# AFAIK there is no limit and no end character so we will usually
# need to wait for timeout
ret = self.serial.read_until(size=20).decode("utf-8")
ret = self.ser.read_until(size=20).decode("utf-8")
except serial.SerialException:
_LOGGER.error("Problem communicating with %s", self._serial_port)
self.serial.close()
self.ser.close()
return ret
def _write_read_format(self, msg: str) -> str:

View File

@@ -136,7 +136,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Tuple to hold data needed for notification
NotificationItem = namedtuple( # noqa: PYI024
NotificationItem = namedtuple(
"NotificationItem", "hnotify huser name plc_datatype callback"
)

View File

@@ -59,7 +59,7 @@ async def async_setup_entry(
platform = async_get_current_platform()
for service, method in CAMERA_SERVICES.items():
platform.async_register_entity_service(service, None, method)
platform.async_register_entity_service(service, {}, method)
class AgentCamera(MjpegCamera):

View File

@@ -21,7 +21,6 @@ PLATFORMS: list[Platform] = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.UPDATE,
]

View File

@@ -92,7 +92,9 @@ class AirGradientConfigFlow(ConfigFlow, domain=DOMAIN):
except AirGradientError:
errors["base"] = "cannot_connect"
else:
await self.async_set_unique_id(current_measures.serial_number)
await self.async_set_unique_id(
current_measures.serial_number, raise_on_progress=False
)
self._abort_if_unique_id_configured()
await self.set_configuration_source()
return self.async_create_entry(

View File

@@ -1,55 +0,0 @@
"""Airgradient Update platform."""
from datetime import timedelta
from functools import cached_property
from homeassistant.components.update import UpdateDeviceClass, UpdateEntity
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AirGradientConfigEntry, AirGradientMeasurementCoordinator
from .entity import AirGradientEntity
SCAN_INTERVAL = timedelta(hours=1)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirGradientConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Airgradient update platform."""
data = config_entry.runtime_data
async_add_entities([AirGradientUpdate(data.measurement)], True)
class AirGradientUpdate(AirGradientEntity, UpdateEntity):
"""Representation of Airgradient Update."""
_attr_device_class = UpdateDeviceClass.FIRMWARE
coordinator: AirGradientMeasurementCoordinator
def __init__(self, coordinator: AirGradientMeasurementCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.serial_number}-update"
@cached_property
def should_poll(self) -> bool:
"""Return True because we need to poll the latest version."""
return True
@property
def installed_version(self) -> str:
"""Return the installed version of the entity."""
return self.coordinator.data.firmware_version
async def async_update(self) -> None:
"""Update the entity."""
self._attr_latest_version = (
await self.coordinator.client.get_latest_firmware_version(
self.coordinator.serial_number
)
)

View File

@@ -1,11 +1,9 @@
"""Config flow for AirTouch4."""
from typing import Any
from airtouch4pyapi import AirTouch, AirTouchStatus
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow
from homeassistant.const import CONF_HOST
from .const import DOMAIN
@@ -18,9 +16,7 @@ class AirtouchConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
if user_input is None:
return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA)

View File

@@ -31,6 +31,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import (
aiohttp_client,
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
@@ -61,6 +62,8 @@ PLATFORMS = [Platform.SENSOR]
DEFAULT_ATTRIBUTION = "Data provided by AirVisual"
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
@callback
def async_get_cloud_api_update_interval(

View File

@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling",
"loggers": ["aioairzone"],
"requirements": ["aioairzone==0.8.2"]
"requirements": ["aioairzone==0.8.1"]
}

View File

@@ -161,11 +161,6 @@ class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
entity_description: AirzoneBinarySensorEntityDescription
@property
def available(self) -> bool:
"""Return Airzone Cloud binary sensor availability."""
return super().available and self.is_on is not None
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""

View File

@@ -189,11 +189,6 @@ async def async_setup_entry(
class AirzoneSensor(AirzoneEntity, SensorEntity):
"""Define an Airzone Cloud sensor."""
@property
def available(self) -> bool:
"""Return Airzone Cloud sensor availability."""
return super().available and self.native_value is not None
@callback
def _handle_coordinator_update(self) -> None:
"""Update attributes when the coordinator updates."""

View File

@@ -124,9 +124,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
if not entities:
return False
component.async_register_entity_service(SERVICE_TURN_OFF, None, "async_turn_off")
component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")
component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle")
component.async_register_entity_service(SERVICE_TURN_OFF, {}, "async_turn_off")
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
await component.async_add_entities(entities)
@@ -162,8 +162,16 @@ class Alert(Entity):
self._data = data
self._message_template = message_template
if self._message_template is not None:
self._message_template.hass = hass
self._done_message_template = done_message_template
if self._done_message_template is not None:
self._done_message_template.hass = hass
self._title_template = title_template
if self._title_template is not None:
self._title_template.hass = hass
self._notifiers = notifiers
self._can_ack = can_ack

View File

@@ -661,9 +661,12 @@ class RemoteCapabilities(AlexaEntity):
def interfaces(self) -> Generator[AlexaCapability]:
"""Yield the supported interfaces."""
yield AlexaPowerController(self.entity)
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
activities = self.entity.attributes.get(remote.ATTR_ACTIVITY_LIST) or []
if activities and supported & remote.RemoteEntityFeature.ACTIVITY:
yield AlexaModeController(
self.entity, instance=f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}"
)
yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity)

View File

@@ -52,6 +52,7 @@ class AlexaFlashBriefingView(http.HomeAssistantView):
"""Initialize Alexa view."""
super().__init__()
self.flash_briefings = flash_briefings
template.attach(hass, self.flash_briefings)
@callback
def get(

View File

@@ -1206,7 +1206,7 @@ async def async_api_set_mode(
raise AlexaInvalidValueError(msg)
# Remote Activity
elif instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
if instance == f"{remote.DOMAIN}.{remote.ATTR_ACTIVITY}":
activity = mode.split(".")[1]
activities: list[str] | None = entity.attributes.get(remote.ATTR_ACTIVITY_LIST)
if activity != PRESET_MODE_NA and activities and activity in activities:

View File

@@ -283,7 +283,7 @@ class AlexaPresetResource(AlexaCapabilityResource):
"""Implements Alexa PresetResources.
Use presetResources with RangeController to provide a set of
friendlyNames for each RangeController preset.
friendlyNamesfor each RangeController preset.
https://developer.amazon.com/docs/device-apis/resources-and-assets.html#presetresources
"""

View File

@@ -194,7 +194,7 @@ async def async_handle_message(
try:
if not enabled:
raise AlexaBridgeUnreachableError( # noqa: TRY301
raise AlexaBridgeUnreachableError(
"Alexa API not enabled in Home Assistant configuration"
)

View File

@@ -8,23 +8,128 @@ CONF_REGION: Final = "region_name"
CONF_ACCESS_KEY_ID: Final = "aws_access_key_id"
CONF_SECRET_ACCESS_KEY: Final = "aws_secret_access_key"
DEFAULT_REGION: Final = "us-east-1"
SUPPORTED_REGIONS: Final[list[str]] = [
"us-east-1",
"us-east-2",
"us-west-1",
"us-west-2",
"ca-central-1",
"eu-west-1",
"eu-central-1",
"eu-west-2",
"eu-west-3",
"ap-southeast-1",
"ap-southeast-2",
"ap-northeast-2",
"ap-northeast-1",
"ap-south-1",
"sa-east-1",
]
CONF_ENGINE: Final = "engine"
CONF_VOICE: Final = "voice"
CONF_OUTPUT_FORMAT: Final = "output_format"
CONF_SAMPLE_RATE: Final = "sample_rate"
CONF_TEXT_TYPE: Final = "text_type"
SUPPORTED_OUTPUT_FORMATS: Final[set[str]] = {"mp3", "ogg_vorbis", "pcm"}
SUPPORTED_VOICES: Final[list[str]] = [
"Aditi", # Hindi
"Amy", # English (British)
"Aria", # English (New Zealand), Neural
"Arlet", # Catalan, Neural
"Arthur", # English, Neural
"Astrid", # Swedish
"Ayanda", # English (South African), Neural
"Bianca", # Italian
"Brian", # English (British)
"Camila", # Portuguese, Brazilian
"Carla", # Italian
"Carmen", # Romanian
"Celine", # French
"Chantal", # French Canadian
"Conchita", # Spanish (European)
"Cristiano", # Portuguese (European)
"Daniel", # German, Neural
"Dora", # Icelandic
"Elin", # Swedish, Neural
"Emma", # English
"Enrique", # Spanish (European)
"Ewa", # Polish
"Filiz", # Turkish
"Gabrielle", # French (Canadian)
"Geraint", # English Welsh
"Giorgio", # Italian
"Gwyneth", # Welsh
"Hala", # Arabic (Gulf), Neural
"Hannah", # German (Austrian), Neural
"Hans", # German
"Hiujin", # Chinese (Cantonese), Neural
"Ida", # Norwegian, Neural
"Ines", # Portuguese, European # codespell:ignore ines
"Ivy", # English
"Jacek", # Polish
"Jan", # Polish
"Joanna", # English
"Joey", # English
"Justin", # English
"Kajal", # English (Indian)/Hindi (Bilingual ), Neural
"Karl", # Icelandic
"Kendra", # English
"Kevin", # English, Neural
"Kimberly", # English
"Laura", # Dutch, Neural
"Lea", # French
"Liam", # Canadian French, Neural
"Liv", # Norwegian
"Lotte", # Dutch
"Lucia", # Spanish European
"Lupe", # Spanish US
"Mads", # Danish
"Maja", # Polish
"Marlene", # German
"Mathieu", # French
"Matthew", # English
"Maxim", # Russian
"Mia", # Spanish Mexican
"Miguel", # Spanish US
"Mizuki", # Japanese
"Naja", # Danish
"Nicole", # English Australian
"Ola", # Polish, Neural
"Olivia", # Female, Australian, Neural
"Penelope", # Spanish US
"Pedro", # Spanish US, Neural
"Raveena", # English, Indian
"Ricardo", # Portuguese (Brazilian)
"Ruben", # Dutch
"Russell", # English (Australian)
"Ruth", # English, Neural
"Salli", # English
"Seoyeon", # Korean
"Stephen", # English, Neural
"Suvi", # Finnish
"Takumi", # Japanese
"Tatyana", # Russian
"Vicki", # German
"Vitoria", # Portuguese, Brazilian
"Zeina", # Arabic
"Zhiyu", # Chinese
]
SUPPORTED_SAMPLE_RATES: Final[set[str]] = {"8000", "16000", "22050", "24000"}
SUPPORTED_OUTPUT_FORMATS: Final[list[str]] = ["mp3", "ogg_vorbis", "pcm"]
SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, set[str]]] = {
"mp3": {"8000", "16000", "22050", "24000"},
"ogg_vorbis": {"8000", "16000", "22050"},
"pcm": {"8000", "16000"},
SUPPORTED_ENGINES: Final[list[str]] = ["neural", "standard"]
SUPPORTED_SAMPLE_RATES: Final[list[str]] = ["8000", "16000", "22050", "24000"]
SUPPORTED_SAMPLE_RATES_MAP: Final[dict[str, list[str]]] = {
"mp3": ["8000", "16000", "22050", "24000"],
"ogg_vorbis": ["8000", "16000", "22050"],
"pcm": ["8000", "16000"],
}
SUPPORTED_TEXT_TYPES: Final[set[str]] = {"text", "ssml"}
SUPPORTED_TEXT_TYPES: Final[list[str]] = ["text", "ssml"]
CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = {
"audio/mpeg": "mp3",
@@ -32,8 +137,6 @@ CONTENT_TYPE_EXTENSIONS: Final[dict[str, str]] = {
"audio/pcm": "pcm",
}
DEFAULT_REGION: Final = "us-east-1"
DEFAULT_ENGINE: Final = "standard"
DEFAULT_VOICE: Final = "Joanna"
DEFAULT_OUTPUT_FORMAT: Final = "mp3"

View File

@@ -16,11 +16,6 @@ from homeassistant.components.tts import (
)
from homeassistant.const import ATTR_CREDENTIALS, CONF_PROFILE_NAME
from homeassistant.core import HomeAssistant
from homeassistant.generated.amazon_polly import (
SUPPORTED_ENGINES,
SUPPORTED_REGIONS,
SUPPORTED_VOICES,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -43,10 +38,13 @@ from .const import (
DEFAULT_SAMPLE_RATES,
DEFAULT_TEXT_TYPE,
DEFAULT_VOICE,
SUPPORTED_ENGINES,
SUPPORTED_OUTPUT_FORMATS,
SUPPORTED_REGIONS,
SUPPORTED_SAMPLE_RATES,
SUPPORTED_SAMPLE_RATES_MAP,
SUPPORTED_TEXT_TYPES,
SUPPORTED_VOICES,
)
_LOGGER: Final = logging.getLogger(__name__)

View File

@@ -17,6 +17,7 @@ from homeassistant.const import (
)
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
import homeassistant.helpers.entity_registry as er
@@ -24,6 +25,7 @@ import homeassistant.helpers.entity_registry as er
from .const import (
ATTR_LAST_DATA,
CONF_APP_KEY,
DOMAIN,
LOGGER,
TYPE_SOLARRADIATION,
TYPE_SOLARRADIATION_LX,
@@ -35,6 +37,7 @@ DATA_CONFIG = "config"
DEFAULT_SOCKET_MIN_RETRY = 15
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
type AmbientStationConfigEntry = ConfigEntry[AmbientStation]

View File

@@ -2,8 +2,6 @@
from __future__ import annotations
from typing import Any
from aioambient import API
from aioambient.errors import AmbientError
import voluptuous as vol
@@ -34,9 +32,7 @@ class AmbientStationFlowHandler(ConfigFlow, domain=DOMAIN):
errors=errors if errors else {},
)
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
async def async_step_user(self, user_input: dict | None = None) -> ConfigFlowResult:
"""Handle the start of the config flow."""
if not user_input:
return await self._show_form()

View File

@@ -499,7 +499,7 @@ class AmcrestCam(Camera):
await getattr(self, f"_async_set_{func}")(value)
new_value = await getattr(self, f"_async_get_{func}")()
if new_value != value:
raise AmcrestCommandFailed # noqa: TRY301
raise AmcrestCommandFailed
except (AmcrestError, AmcrestCommandFailed) as error:
if tries == 1:
log_update_error(_LOGGER, action, self.name, description, error)

View File

@@ -14,6 +14,7 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN
from .coordinator import AndroidIPCamDataUpdateCoordinator
@@ -26,6 +27,9 @@ PLATFORMS: list[Platform] = [
]
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Android IP Webcam from a config entry."""
websession = async_get_clientsession(hass)

View File

@@ -87,7 +87,7 @@ async def async_setup_entry(
"adb_command",
)
platform.async_register_entity_service(
SERVICE_LEARN_SENDEVENT, None, "learn_sendevent"
SERVICE_LEARN_SENDEVENT, {}, "learn_sendevent"
)
platform.async_register_entity_service(
SERVICE_DOWNLOAD,

View File

@@ -1,46 +0,0 @@
"""The Anthropic integration."""
from __future__ import annotations
import anthropic
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from .const import DOMAIN, LOGGER
PLATFORMS = (Platform.CONVERSATION,)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
type AnthropicConfigEntry = ConfigEntry[anthropic.AsyncClient]
async def async_setup_entry(hass: HomeAssistant, entry: AnthropicConfigEntry) -> bool:
"""Set up Anthropic from a config entry."""
client = anthropic.AsyncAnthropic(api_key=entry.data[CONF_API_KEY])
try:
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
except anthropic.AuthenticationError as err:
LOGGER.error("Invalid API key: %s", err)
return False
except anthropic.AnthropicError as err:
raise ConfigEntryNotReady(err) from err
entry.runtime_data = client
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Anthropic."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,210 +0,0 @@
"""Config flow for Anthropic integration."""
from __future__ import annotations
import logging
from types import MappingProxyType
from typing import Any
import anthropic
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_API_KEY, CONF_LLM_HASS_API
from homeassistant.core import HomeAssistant
from homeassistant.helpers import llm
from homeassistant.helpers.selector import (
NumberSelector,
NumberSelectorConfig,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
TemplateSelector,
)
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_RECOMMENDED,
CONF_TEMPERATURE,
DOMAIN,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
)
_LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): str,
}
)
RECOMMENDED_OPTIONS = {
CONF_RECOMMENDED: True,
CONF_LLM_HASS_API: llm.LLM_API_ASSIST,
CONF_PROMPT: llm.DEFAULT_INSTRUCTIONS_PROMPT,
}
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
"""Validate the user input allows us to connect.
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
"""
client = anthropic.AsyncAnthropic(api_key=data[CONF_API_KEY])
await client.messages.create(
model="claude-3-haiku-20240307",
max_tokens=1,
messages=[{"role": "user", "content": "Hi"}],
timeout=10.0,
)
class AnthropicConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Anthropic."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
await validate_input(self.hass, user_input)
except anthropic.APITimeoutError:
errors["base"] = "timeout_connect"
except anthropic.APIConnectionError:
errors["base"] = "cannot_connect"
except anthropic.APIStatusError as e:
if isinstance(e.body, dict):
errors["base"] = e.body.get("error", {}).get("type", "unknown")
else:
errors["base"] = "unknown"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_create_entry(
title="Claude",
data=user_input,
options=RECOMMENDED_OPTIONS,
)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors or None
)
@staticmethod
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
"""Create the options flow."""
return AnthropicOptionsFlow(config_entry)
class AnthropicOptionsFlow(OptionsFlow):
"""Anthropic config flow options handler."""
def __init__(self, config_entry: ConfigEntry) -> None:
"""Initialize options flow."""
self.config_entry = config_entry
self.last_rendered_recommended = config_entry.options.get(
CONF_RECOMMENDED, False
)
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the options."""
options: dict[str, Any] | MappingProxyType[str, Any] = self.config_entry.options
if user_input is not None:
if user_input[CONF_RECOMMENDED] == self.last_rendered_recommended:
if user_input[CONF_LLM_HASS_API] == "none":
user_input.pop(CONF_LLM_HASS_API)
return self.async_create_entry(title="", data=user_input)
# Re-render the options again, now with the recommended options shown/hidden
self.last_rendered_recommended = user_input[CONF_RECOMMENDED]
options = {
CONF_RECOMMENDED: user_input[CONF_RECOMMENDED],
CONF_PROMPT: user_input[CONF_PROMPT],
CONF_LLM_HASS_API: user_input[CONF_LLM_HASS_API],
}
suggested_values = options.copy()
if not suggested_values.get(CONF_PROMPT):
suggested_values[CONF_PROMPT] = llm.DEFAULT_INSTRUCTIONS_PROMPT
schema = self.add_suggested_values_to_schema(
vol.Schema(anthropic_config_option_schema(self.hass, options)),
suggested_values,
)
return self.async_show_form(
step_id="init",
data_schema=schema,
)
def anthropic_config_option_schema(
hass: HomeAssistant,
options: dict[str, Any] | MappingProxyType[str, Any],
) -> dict:
"""Return a schema for Anthropic completion options."""
hass_apis: list[SelectOptionDict] = [
SelectOptionDict(
label="No control",
value="none",
)
]
hass_apis.extend(
SelectOptionDict(
label=api.name,
value=api.id,
)
for api in llm.async_get_apis(hass)
)
schema = {
vol.Optional(CONF_PROMPT): TemplateSelector(),
vol.Optional(CONF_LLM_HASS_API, default="none"): SelectSelector(
SelectSelectorConfig(options=hass_apis)
),
vol.Required(
CONF_RECOMMENDED, default=options.get(CONF_RECOMMENDED, False)
): bool,
}
if options.get(CONF_RECOMMENDED):
return schema
schema.update(
{
vol.Optional(
CONF_CHAT_MODEL,
default=RECOMMENDED_CHAT_MODEL,
): str,
vol.Optional(
CONF_MAX_TOKENS,
default=RECOMMENDED_MAX_TOKENS,
): int,
vol.Optional(
CONF_TEMPERATURE,
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=1, step=0.05)),
}
)
return schema

View File

@@ -1,15 +0,0 @@
"""Constants for the Anthropic integration."""
import logging
DOMAIN = "anthropic"
LOGGER = logging.getLogger(__package__)
CONF_RECOMMENDED = "recommended"
CONF_PROMPT = "prompt"
CONF_CHAT_MODEL = "chat_model"
RECOMMENDED_CHAT_MODEL = "claude-3-5-sonnet-20240620"
CONF_MAX_TOKENS = "max_tokens"
RECOMMENDED_MAX_TOKENS = 1024
CONF_TEMPERATURE = "temperature"
RECOMMENDED_TEMPERATURE = 1.0

View File

@@ -1,316 +0,0 @@
"""Conversation support for Anthropic."""
from collections.abc import Callable
import json
from typing import Any, Literal, cast
import anthropic
from anthropic._types import NOT_GIVEN
from anthropic.types import (
Message,
MessageParam,
TextBlock,
TextBlockParam,
ToolParam,
ToolResultBlockParam,
ToolUseBlock,
ToolUseBlockParam,
)
import voluptuous as vol
from voluptuous_openapi import convert
from homeassistant.components import conversation
from homeassistant.components.conversation import trace
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_LLM_HASS_API, MATCH_ALL
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, TemplateError
from homeassistant.helpers import device_registry as dr, intent, llm, template
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import ulid
from . import AnthropicConfigEntry
from .const import (
CONF_CHAT_MODEL,
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_TEMPERATURE,
DOMAIN,
LOGGER,
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_TEMPERATURE,
)
# Max number of back and forth with the LLM to generate a response
MAX_TOOL_ITERATIONS = 10
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AnthropicConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = AnthropicConversationEntity(config_entry)
async_add_entities([agent])
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
) -> ToolParam:
"""Format tool specification."""
return ToolParam(
name=tool.name,
description=tool.description or "",
input_schema=convert(tool.parameters, custom_serializer=custom_serializer),
)
def _message_convert(
message: Message,
) -> MessageParam:
"""Convert from class to TypedDict."""
param_content: list[TextBlockParam | ToolUseBlockParam] = []
for message_content in message.content:
if isinstance(message_content, TextBlock):
param_content.append(TextBlockParam(type="text", text=message_content.text))
elif isinstance(message_content, ToolUseBlock):
param_content.append(
ToolUseBlockParam(
type="tool_use",
id=message_content.id,
name=message_content.name,
input=message_content.input,
)
)
return MessageParam(role=message.role, content=param_content)
class AnthropicConversationEntity(
conversation.ConversationEntity, conversation.AbstractConversationAgent
):
"""Anthropic conversation agent."""
_attr_has_entity_name = True
_attr_name = None
def __init__(self, entry: AnthropicConfigEntry) -> None:
"""Initialize the agent."""
self.entry = entry
self.history: dict[str, list[MessageParam]] = {}
self._attr_unique_id = entry.entry_id
self._attr_device_info = dr.DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)},
manufacturer="Anthropic",
model="Claude",
entry_type=dr.DeviceEntryType.SERVICE,
)
if self.entry.options.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
@property
def supported_languages(self) -> list[str] | Literal["*"]:
"""Return a list of supported languages."""
return MATCH_ALL
async def async_added_to_hass(self) -> None:
"""When entity is added to Home Assistant."""
await super().async_added_to_hass()
self.entry.async_on_unload(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def async_process(
self, user_input: conversation.ConversationInput
) -> conversation.ConversationResult:
"""Process a sentence."""
options = self.entry.options
intent_response = intent.IntentResponse(language=user_input.language)
llm_api: llm.APIInstance | None = None
tools: list[ToolParam] | None = None
user_name: str | None = None
llm_context = llm.LLMContext(
platform=DOMAIN,
context=user_input.context,
user_prompt=user_input.text,
language=user_input.language,
assistant=conversation.DOMAIN,
device_id=user_input.device_id,
)
if options.get(CONF_LLM_HASS_API):
try:
llm_api = await llm.async_get_api(
self.hass,
options[CONF_LLM_HASS_API],
llm_context,
)
except HomeAssistantError as err:
LOGGER.error("Error getting LLM API: %s", err)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Error preparing LLM API: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=user_input.conversation_id
)
tools = [
_format_tool(tool, llm_api.custom_serializer) for tool in llm_api.tools
]
if user_input.conversation_id is None:
conversation_id = ulid.ulid_now()
messages = []
elif user_input.conversation_id in self.history:
conversation_id = user_input.conversation_id
messages = self.history[conversation_id]
else:
# Conversation IDs are ULIDs. We generate a new one if not provided.
# If an old OLID is passed in, we will generate a new one to indicate
# a new conversation was started. If the user picks their own, they
# want to track a conversation and we respect it.
try:
ulid.ulid_to_bytes(user_input.conversation_id)
conversation_id = ulid.ulid_now()
except ValueError:
conversation_id = user_input.conversation_id
messages = []
if (
user_input.context
and user_input.context.user_id
and (
user := await self.hass.auth.async_get_user(user_input.context.user_id)
)
):
user_name = user.name
try:
prompt_parts = [
template.Template(
llm.BASE_PROMPT
+ options.get(CONF_PROMPT, llm.DEFAULT_INSTRUCTIONS_PROMPT),
self.hass,
).async_render(
{
"ha_name": self.hass.config.location_name,
"user_name": user_name,
"llm_context": llm_context,
},
parse_result=False,
)
]
except TemplateError as err:
LOGGER.error("Error rendering prompt: %s", err)
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem with my template: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
if llm_api:
prompt_parts.append(llm_api.api_prompt)
prompt = "\n".join(prompt_parts)
# Create a copy of the variable because we attach it to the trace
messages = [*messages, MessageParam(role="user", content=user_input.text)]
LOGGER.debug("Prompt: %s", messages)
LOGGER.debug("Tools: %s", tools)
trace.async_conversation_trace_append(
trace.ConversationTraceEventType.AGENT_DETAIL,
{"system": prompt, "messages": messages},
)
client = self.entry.runtime_data
# To prevent infinite loops, we limit the number of iterations
for _iteration in range(MAX_TOOL_ITERATIONS):
try:
response = await client.messages.create(
model=options.get(CONF_CHAT_MODEL, RECOMMENDED_CHAT_MODEL),
messages=messages,
tools=tools or NOT_GIVEN,
max_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
system=prompt,
temperature=options.get(CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE),
)
except anthropic.AnthropicError as err:
intent_response.async_set_error(
intent.IntentResponseErrorCode.UNKNOWN,
f"Sorry, I had a problem talking to Anthropic: {err}",
)
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
LOGGER.debug("Response %s", response)
messages.append(_message_convert(response))
if response.stop_reason != "tool_use" or not llm_api:
break
tool_results: list[ToolResultBlockParam] = []
for tool_call in response.content:
if isinstance(tool_call, TextBlock):
LOGGER.info(tool_call.text)
if not isinstance(tool_call, ToolUseBlock):
continue
tool_input = llm.ToolInput(
tool_name=tool_call.name,
tool_args=cast(dict[str, Any], tool_call.input),
)
LOGGER.debug(
"Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args
)
try:
tool_response = await llm_api.async_call_tool(tool_input)
except (HomeAssistantError, vol.Invalid) as e:
tool_response = {"error": type(e).__name__}
if str(e):
tool_response["error_text"] = str(e)
LOGGER.debug("Tool response: %s", tool_response)
tool_results.append(
ToolResultBlockParam(
type="tool_result",
tool_use_id=tool_call.id,
content=json.dumps(tool_response),
)
)
messages.append(MessageParam(role="user", content=tool_results))
self.history[conversation_id] = messages
for content in response.content:
if isinstance(content, TextBlock):
intent_response.async_set_speech(content.text)
break
return conversation.ConversationResult(
response=intent_response, conversation_id=conversation_id
)
async def _async_entry_update_listener(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -1,12 +0,0 @@
{
"domain": "anthropic",
"name": "Anthropic Conversation",
"after_dependencies": ["assist_pipeline", "intent"],
"codeowners": ["@Shulyaka"],
"config_flow": true,
"dependencies": ["conversation"],
"documentation": "https://www.home-assistant.io/integrations/anthropic",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["anthropic==0.31.2"]
}

View File

@@ -1,34 +0,0 @@
{
"config": {
"step": {
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"timeout_connect": "[%key:common::config_flow::error::timeout_connect%]",
"authentication_error": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
},
"options": {
"step": {
"init": {
"data": {
"prompt": "Instructions",
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "Maximum tokens to return in response",
"temperature": "Temperature",
"llm_hass_api": "[%key:common::config_flow::data::llm_hass_api%]",
"recommended": "Recommended model settings"
},
"data_description": {
"prompt": "Instruct how the LLM should respond. This can be a template."
}
}
}
}
}

View File

@@ -8,6 +8,7 @@ from typing import Final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN
from .coordinator import APCUPSdCoordinator
@@ -16,6 +17,8 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS: Final = (Platform.BINARY_SENSOR, Platform.SENSOR)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Use config values to set up a function enabling status retrieval."""

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push",
"loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.15.0"],
"requirements": ["pyatv==0.14.3"],
"zeroconf": [
"_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.",

View File

@@ -13,12 +13,7 @@ from homeassistant.core import HomeAssistant
from .const import DEFAULT_PORT
from .coordinator import ApSystemsDataCoordinator
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
]
PLATFORMS: list[Platform] = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH]
@dataclass

View File

@@ -1,102 +0,0 @@
"""The read-only binary sensors for APsystems local API integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from APsystemsEZ1 import ReturnAlarmInfo
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import ApSystemsConfigEntry, ApSystemsData
from .coordinator import ApSystemsDataCoordinator
from .entity import ApSystemsEntity
@dataclass(frozen=True, kw_only=True)
class ApsystemsLocalApiBinarySensorDescription(BinarySensorEntityDescription):
"""Describes Apsystens Inverter binary sensor entity."""
is_on: Callable[[ReturnAlarmInfo], bool | None]
BINARY_SENSORS: tuple[ApsystemsLocalApiBinarySensorDescription, ...] = (
ApsystemsLocalApiBinarySensorDescription(
key="off_grid_status",
translation_key="off_grid_status",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
is_on=lambda c: c.offgrid,
),
ApsystemsLocalApiBinarySensorDescription(
key="dc_1_short_circuit_error_status",
translation_key="dc_1_short_circuit_error_status",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
is_on=lambda c: c.shortcircuit_1,
),
ApsystemsLocalApiBinarySensorDescription(
key="dc_2_short_circuit_error_status",
translation_key="dc_2_short_circuit_error_status",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
is_on=lambda c: c.shortcircuit_2,
),
ApsystemsLocalApiBinarySensorDescription(
key="output_fault_status",
translation_key="output_fault_status",
device_class=BinarySensorDeviceClass.PROBLEM,
entity_category=EntityCategory.DIAGNOSTIC,
is_on=lambda c: not c.operating,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ApSystemsConfigEntry,
add_entities: AddEntitiesCallback,
) -> None:
"""Set up the binary sensor platform."""
config = config_entry.runtime_data
add_entities(
ApSystemsBinarySensorWithDescription(
data=config,
entity_description=desc,
)
for desc in BINARY_SENSORS
)
class ApSystemsBinarySensorWithDescription(
CoordinatorEntity[ApSystemsDataCoordinator], ApSystemsEntity, BinarySensorEntity
):
"""Base binary sensor to be used with description."""
entity_description: ApsystemsLocalApiBinarySensorDescription
def __init__(
self,
data: ApSystemsData,
entity_description: ApsystemsLocalApiBinarySensorDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(data.coordinator)
ApSystemsEntity.__init__(self, data)
self.entity_description = entity_description
self._attr_unique_id = f"{data.device_id}_{entity_description.key}"
@property
def is_on(self) -> bool | None:
"""Return value of sensor."""
return self.entity_description.is_on(self.coordinator.data.alarm_info)

View File

@@ -2,26 +2,17 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData
from APsystemsEZ1 import APsystemsEZ1M, ReturnOutputData
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import LOGGER
@dataclass
class ApSystemsSensorData:
"""Representing different Apsystems sensor data."""
output_data: ReturnOutputData
alarm_info: ReturnAlarmInfo
class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
class ApSystemsDataCoordinator(DataUpdateCoordinator[ReturnOutputData]):
"""Coordinator used for all sensors."""
def __init__(self, hass: HomeAssistant, api: APsystemsEZ1M) -> None:
@@ -34,14 +25,5 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
)
self.api = api
async def _async_setup(self) -> None:
try:
max_power = (await self.api.get_device_info()).maxPower
except (ConnectionError, TimeoutError):
raise UpdateFailed from None
self.api.max_power = max_power
async def _async_update_data(self) -> ApSystemsSensorData:
output_data = await self.api.get_output_data()
alarm_info = await self.api.get_alarm_info()
return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info)
async def _async_update_data(self) -> ReturnOutputData:
return await self.api.get_output_data()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==2.2.1"]
"requirements": ["apsystems-ez1==1.3.3"]
}

View File

@@ -26,6 +26,7 @@ async def async_setup_entry(
class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
"""Base sensor to be used with description."""
_attr_native_max_value = 800
_attr_native_min_value = 30
_attr_native_step = 1
_attr_device_class = NumberDeviceClass.POWER
@@ -41,7 +42,6 @@ class ApSystemsMaxOutputNumber(ApSystemsEntity, NumberEntity):
super().__init__(data)
self._api = data.coordinator.api
self._attr_unique_id = f"{data.device_id}_output_limit"
self._attr_native_max_value = data.coordinator.api.max_power
async def async_update(self) -> None:
"""Set the state with the value fetched from the inverter."""

View File

@@ -148,4 +148,4 @@ class ApSystemsSensorWithDescription(
@property
def native_value(self) -> StateType:
"""Return value of sensor."""
return self.entity_description.value_fn(self.coordinator.data.output_data)
return self.entity_description.value_fn(self.coordinator.data)

View File

@@ -19,20 +19,6 @@
}
},
"entity": {
"binary_sensor": {
"off_grid_status": {
"name": "Off grid status"
},
"dc_1_short_circuit_error_status": {
"name": "DC 1 short circuit error status"
},
"dc_2_short_circuit_error_status": {
"name": "DC 2 short circuit error status"
},
"output_fault_status": {
"name": "Output fault status"
}
},
"sensor": {
"total_power": {
"name": "Total power"

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any
from aiohttp.client_exceptions import ClientConnectionError
from APsystemsEZ1 import Status
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.core import HomeAssistant
@@ -44,12 +45,12 @@ class ApSystemsInverterSwitch(ApSystemsEntity, SwitchEntity):
self._attr_available = False
else:
self._attr_available = True
self._attr_is_on = status
self._attr_is_on = status == Status.normal
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self._api.set_device_power_status(True)
await self._api.set_device_power_status(0)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self._api.set_device_power_status(False)
await self._api.set_device_power_status(1)

View File

@@ -3,14 +3,12 @@
from __future__ import annotations
from aioaquacell import AquacellApi
from aioaquacell.const import Brand
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_BRAND
from .coordinator import AquacellCoordinator
PLATFORMS = [Platform.SENSOR]
@@ -22,9 +20,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: AquacellConfigEntry) ->
"""Set up Aquacell from a config entry."""
session = async_get_clientsession(hass)
brand = entry.data.get(CONF_BRAND, Brand.AQUACELL)
aquacell_api = AquacellApi(session, brand)
aquacell_api = AquacellApi(session)
coordinator = AquacellCoordinator(hass, aquacell_api)

View File

@@ -7,27 +7,18 @@ import logging
from typing import Any
from aioaquacell import ApiException, AquacellApi, AuthenticationFailed
from aioaquacell.const import SUPPORTED_BRANDS, Brand
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import (
CONF_BRAND,
CONF_REFRESH_TOKEN,
CONF_REFRESH_TOKEN_CREATION_TIME,
DOMAIN,
)
from .const import CONF_REFRESH_TOKEN, CONF_REFRESH_TOKEN_CREATION_TIME, DOMAIN
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_BRAND, default=Brand.AQUACELL): vol.In(
{key: brand.name for key, brand in SUPPORTED_BRANDS.items()}
),
vol.Required(CONF_EMAIL): str,
vol.Required(CONF_PASSWORD): str,
}
@@ -42,7 +33,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the cloud logon step."""
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
await self.async_set_unique_id(
@@ -51,7 +42,7 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
self._abort_if_unique_id_configured()
session = async_get_clientsession(self.hass)
api = AquacellApi(session, user_input[CONF_BRAND])
api = AquacellApi(session)
try:
refresh_token = await api.authenticate(
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
@@ -68,7 +59,6 @@ class AquaCellConfigFlow(ConfigFlow, domain=DOMAIN):
title=user_input[CONF_EMAIL],
data={
**user_input,
CONF_BRAND: user_input[CONF_BRAND],
CONF_REFRESH_TOKEN: refresh_token,
CONF_REFRESH_TOKEN_CREATION_TIME: datetime.now().timestamp(),
},

View File

@@ -5,7 +5,6 @@ from datetime import timedelta
DOMAIN = "aquacell"
DATA_AQUACELL = "DATA_AQUACELL"
CONF_BRAND = "brand"
CONF_REFRESH_TOKEN = "refresh_token"
CONF_REFRESH_TOKEN_CREATION_TIME = "refresh_token_creation_time"

View File

@@ -1,6 +1,6 @@
{
"domain": "aquacell",
"name": "AquaCell",
"name": "Aquacell",
"codeowners": ["@Jordi1990"],
"config_flow": true,
"dependencies": ["http", "network"],

View File

@@ -2,9 +2,8 @@
"config": {
"step": {
"user": {
"description": "Select the brand of the softener and fill in your softener mobile app credentials",
"description": "Fill in your Aquacell mobile app credentials",
"data": {
"brand": "Brand",
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
}

View File

@@ -6,9 +6,6 @@
},
"radiation_rate": {
"default": "mdi:radioactive"
},
"radon_concentration": {
"default": "mdi:radioactive"
}
}
}

View File

@@ -19,5 +19,5 @@
"documentation": "https://www.home-assistant.io/integrations/aranet",
"integration_type": "device",
"iot_class": "local_push",
"requirements": ["aranet4==2.4.0"]
"requirements": ["aranet4==2.3.4"]
}

View File

@@ -99,13 +99,6 @@ SENSOR_DESCRIPTIONS = {
suggested_display_precision=4,
scale=0.000001,
),
"radon_concentration": AranetSensorEntityDescription(
key="radon_concentration",
translation_key="radon_concentration",
name="Radon Concentration",
native_unit_of_measurement="Bq/m³",
state_class=SensorStateClass.MEASUREMENT,
),
"battery": AranetSensorEntityDescription(
key="battery",
name="Battery",

View File

@@ -11,10 +11,12 @@ from arcam.fmj.client import Client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
SIGNAL_CLIENT_DATA,
SIGNAL_CLIENT_STARTED,
SIGNAL_CLIENT_STOPPED,
@@ -24,6 +26,7 @@ type ArcamFmjConfigEntry = ConfigEntry[Client]
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
PLATFORMS = [Platform.MEDIA_PLAYER]

View File

@@ -87,6 +87,8 @@ def setup_platform(
if value_template is None:
return lambda value: value
value_template.hass = hass
def _render(value):
try:
return value_template.async_render({"value": value}, parse_result=False)

View File

@@ -1 +0,0 @@
"""Virtual integration: ArtSound."""

View File

@@ -1,6 +0,0 @@
{
"domain": "artsound",
"name": "ArtSound",
"integration_type": "virtual",
"supported_by": "linkplay"
}

View File

@@ -6,11 +6,13 @@ from collections.abc import Callable, Iterable
from dataclasses import dataclass
from enum import StrEnum
import logging
from .const import SAMPLE_CHANNELS, SAMPLE_RATE, SAMPLE_WIDTH
from typing import Final
_LOGGER = logging.getLogger(__name__)
_SAMPLE_RATE: Final = 16000 # Hz
_SAMPLE_WIDTH: Final = 2 # bytes
class VadSensitivity(StrEnum):
"""How quickly the end of a voice command is detected."""
@@ -24,12 +26,12 @@ class VadSensitivity(StrEnum):
"""Return seconds of silence for sensitivity level."""
sensitivity = VadSensitivity(sensitivity)
if sensitivity == VadSensitivity.RELAXED:
return 1.25
return 2.0
if sensitivity == VadSensitivity.AGGRESSIVE:
return 0.25
return 0.5
return 0.7
return 1.0
class AudioBuffer:
@@ -78,7 +80,7 @@ class VoiceCommandSegmenter:
speech_seconds: float = 0.3
"""Seconds of speech before voice command has started."""
silence_seconds: float = 0.7
silence_seconds: float = 1.0
"""Seconds of silence after voice command has ended."""
timeout_seconds: float = 15.0
@@ -90,9 +92,6 @@ class VoiceCommandSegmenter:
in_command: bool = False
"""True if inside voice command."""
timed_out: bool = False
"""True a timeout occurred during voice command."""
_speech_seconds_left: float = 0.0
"""Seconds left before considering voice command as started."""
@@ -122,9 +121,6 @@ class VoiceCommandSegmenter:
Returns False when command is done.
"""
if self.timed_out:
self.timed_out = False
self._timeout_seconds_left -= chunk_seconds
if self._timeout_seconds_left <= 0:
_LOGGER.warning(
@@ -132,7 +128,6 @@ class VoiceCommandSegmenter:
self.timeout_seconds,
)
self.reset()
self.timed_out = True
return False
if not self.in_command:
@@ -184,9 +179,7 @@ class VoiceCommandSegmenter:
"""
if vad_samples_per_chunk is None:
# No chunking
chunk_seconds = (
len(chunk) // (SAMPLE_WIDTH * SAMPLE_CHANNELS)
) / SAMPLE_RATE
chunk_seconds = (len(chunk) // _SAMPLE_WIDTH) / _SAMPLE_RATE
is_speech = vad_is_speech(chunk)
return self.process(chunk_seconds, is_speech)
@@ -194,8 +187,8 @@ class VoiceCommandSegmenter:
raise ValueError("leftover_chunk_buffer is required when vad uses chunking")
# With chunking
seconds_per_chunk = vad_samples_per_chunk / SAMPLE_RATE
bytes_per_chunk = vad_samples_per_chunk * (SAMPLE_WIDTH * SAMPLE_CHANNELS)
seconds_per_chunk = vad_samples_per_chunk / _SAMPLE_RATE
bytes_per_chunk = vad_samples_per_chunk * _SAMPLE_WIDTH
for vad_chunk in chunk_samples(chunk, bytes_per_chunk, leftover_chunk_buffer):
is_speech = vad_is_speech(vad_chunk)
if not self.process(seconds_per_chunk, is_speech):

View File

@@ -0,0 +1 @@
"""The asterisk_cdr component."""

View File

@@ -0,0 +1,70 @@
"""Support for the Asterisk CDR interface."""
from __future__ import annotations
import datetime
import hashlib
from typing import Any
from homeassistant.components.asterisk_mbox import (
DOMAIN as ASTERISK_DOMAIN,
SIGNAL_CDR_UPDATE,
)
from homeassistant.components.mailbox import Mailbox
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
MAILBOX_NAME = "asterisk_cdr"
async def async_get_handler(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> Mailbox:
"""Set up the Asterix CDR platform."""
return AsteriskCDR(hass, MAILBOX_NAME)
class AsteriskCDR(Mailbox):
"""Asterisk VM Call Data Record mailbox."""
def __init__(self, hass: HomeAssistant, name: str) -> None:
"""Initialize Asterisk CDR."""
super().__init__(hass, name)
self.cdr: list[dict[str, Any]] = []
async_dispatcher_connect(self.hass, SIGNAL_CDR_UPDATE, self._update_callback)
@callback
def _update_callback(self, msg: list[dict[str, Any]]) -> Any:
"""Update the message count in HA, if needed."""
self._build_message()
self.async_update()
def _build_message(self) -> None:
"""Build message structure."""
cdr: list[dict[str, Any]] = []
for entry in self.hass.data[ASTERISK_DOMAIN].cdr:
timestamp = datetime.datetime.strptime(
entry["time"], "%Y-%m-%d %H:%M:%S"
).timestamp()
info = {
"origtime": timestamp,
"callerid": entry["callerid"],
"duration": entry["duration"],
}
sha = hashlib.sha256(str(entry).encode("utf-8")).hexdigest()
msg = (
f"Destination: {entry['dest']}\n"
f"Application: {entry['application']}\n "
f"Context: {entry['context']}"
)
cdr.append({"info": info, "sha": sha, "text": msg})
self.cdr = cdr
async def async_get_messages(self) -> list[dict[str, Any]]:
"""Return a list of the current messages."""
if not self.cdr:
self._build_message()
return self.cdr

View File

@@ -0,0 +1,8 @@
{
"domain": "asterisk_cdr",
"name": "Asterisk Call Detail Records",
"codeowners": [],
"dependencies": ["asterisk_mbox"],
"documentation": "https://www.home-assistant.io/integrations/asterisk_cdr",
"iot_class": "local_polling"
}

View File

@@ -0,0 +1,153 @@
"""Support for Asterisk Voicemail interface."""
import logging
from typing import Any, cast
from asterisk_mbox import Client as asteriskClient
from asterisk_mbox.commands import (
CMD_MESSAGE_CDR,
CMD_MESSAGE_CDR_AVAILABLE,
CMD_MESSAGE_LIST,
)
import voluptuous as vol
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_connect
from homeassistant.helpers.issue_registry import IssueSeverity, create_issue
from homeassistant.helpers.typing import ConfigType
_LOGGER = logging.getLogger(__name__)
DOMAIN = "asterisk_mbox"
SIGNAL_DISCOVER_PLATFORM = "asterisk_mbox.discover_platform"
SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request"
SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated"
SIGNAL_CDR_UPDATE = "asterisk_mbox.message_updated"
SIGNAL_CDR_REQUEST = "asterisk_mbox.message_request"
CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_PORT): cv.port,
}
)
},
extra=vol.ALLOW_EXTRA,
)
def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up for the Asterisk Voicemail box."""
conf: dict[str, Any] = config[DOMAIN]
host: str = conf[CONF_HOST]
port: int = conf[CONF_PORT]
password: str = conf[CONF_PASSWORD]
hass.data[DOMAIN] = AsteriskData(hass, host, port, password, config)
create_issue(
hass,
DOMAIN,
"deprecated_integration",
breaks_in_ha_version="2024.9.0",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.WARNING,
translation_key="deprecated_integration",
translation_placeholders={
"domain": DOMAIN,
"integration_title": "Asterisk Voicemail",
"mailbox": "mailbox",
},
)
return True
class AsteriskData:
"""Store Asterisk mailbox data."""
def __init__(
self,
hass: HomeAssistant,
host: str,
port: int,
password: str,
config: dict[str, Any],
) -> None:
"""Init the Asterisk data object."""
self.hass = hass
self.config = config
self.messages: list[dict[str, Any]] | None = None
self.cdr: list[dict[str, Any]] | None = None
dispatcher_connect(self.hass, SIGNAL_MESSAGE_REQUEST, self._request_messages)
dispatcher_connect(self.hass, SIGNAL_CDR_REQUEST, self._request_cdr)
dispatcher_connect(self.hass, SIGNAL_DISCOVER_PLATFORM, self._discover_platform)
# Only connect after signal connection to ensure we don't miss any
self.client = asteriskClient(host, port, password, self.handle_data)
@callback
def _discover_platform(self, component: str) -> None:
_LOGGER.debug("Adding mailbox %s", component)
self.hass.async_create_task(
discovery.async_load_platform(
self.hass, "mailbox", component, {}, self.config
)
)
@callback
def handle_data(
self, command: int, msg: list[dict[str, Any]] | dict[str, Any]
) -> None:
"""Handle changes to the mailbox."""
if command == CMD_MESSAGE_LIST:
msg = cast(list[dict[str, Any]], msg)
_LOGGER.debug("AsteriskVM sent updated message list: Len %d", len(msg))
old_messages = self.messages
self.messages = sorted(
msg, key=lambda item: item["info"]["origtime"], reverse=True
)
if not isinstance(old_messages, list):
async_dispatcher_send(self.hass, SIGNAL_DISCOVER_PLATFORM, DOMAIN)
async_dispatcher_send(self.hass, SIGNAL_MESSAGE_UPDATE, self.messages)
elif command == CMD_MESSAGE_CDR:
msg = cast(dict[str, Any], msg)
_LOGGER.debug(
"AsteriskVM sent updated CDR list: Len %d", len(msg.get("entries", []))
)
self.cdr = msg["entries"]
async_dispatcher_send(self.hass, SIGNAL_CDR_UPDATE, self.cdr)
elif command == CMD_MESSAGE_CDR_AVAILABLE:
if not isinstance(self.cdr, list):
_LOGGER.debug("AsteriskVM adding CDR platform")
self.cdr = []
async_dispatcher_send(
self.hass, SIGNAL_DISCOVER_PLATFORM, "asterisk_cdr"
)
async_dispatcher_send(self.hass, SIGNAL_CDR_REQUEST)
else:
_LOGGER.debug(
"AsteriskVM sent unknown message '%d' len: %d", command, len(msg)
)
@callback
def _request_messages(self) -> None:
"""Handle changes to the mailbox."""
_LOGGER.debug("Requesting message list")
self.client.messages()
@callback
def _request_cdr(self) -> None:
"""Handle changes to the CDR."""
_LOGGER.debug("Requesting CDR list")
self.client.get_cdr()

View File

@@ -0,0 +1,86 @@
"""Support for the Asterisk Voicemail interface."""
from __future__ import annotations
from functools import partial
import logging
from typing import Any
from asterisk_mbox import ServerError
from homeassistant.components.mailbox import CONTENT_TYPE_MPEG, Mailbox, StreamError
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN as ASTERISK_DOMAIN, AsteriskData
_LOGGER = logging.getLogger(__name__)
SIGNAL_MESSAGE_REQUEST = "asterisk_mbox.message_request"
SIGNAL_MESSAGE_UPDATE = "asterisk_mbox.message_updated"
async def async_get_handler(
hass: HomeAssistant,
config: ConfigType,
discovery_info: DiscoveryInfoType | None = None,
) -> Mailbox:
"""Set up the Asterix VM platform."""
return AsteriskMailbox(hass, ASTERISK_DOMAIN)
class AsteriskMailbox(Mailbox):
"""Asterisk VM Sensor."""
def __init__(self, hass: HomeAssistant, name: str) -> None:
"""Initialize Asterisk mailbox."""
super().__init__(hass, name)
async_dispatcher_connect(
self.hass, SIGNAL_MESSAGE_UPDATE, self._update_callback
)
@callback
def _update_callback(self, msg: str) -> None:
"""Update the message count in HA, if needed."""
self.async_update()
@property
def media_type(self) -> str:
"""Return the supported media type."""
return CONTENT_TYPE_MPEG
@property
def can_delete(self) -> bool:
"""Return if messages can be deleted."""
return True
@property
def has_media(self) -> bool:
"""Return if messages have attached media files."""
return True
async def async_get_media(self, msgid: str) -> bytes:
"""Return the media blob for the msgid."""
data: AsteriskData = self.hass.data[ASTERISK_DOMAIN]
client = data.client
try:
return await self.hass.async_add_executor_job(
partial(client.mp3, msgid, sync=True)
)
except ServerError as err:
raise StreamError(err) from err
async def async_get_messages(self) -> list[dict[str, Any]]:
"""Return a list of the current messages."""
data: AsteriskData = self.hass.data[ASTERISK_DOMAIN]
return data.messages or []
async def async_delete(self, msgid: str) -> bool:
"""Delete the specified messages."""
data: AsteriskData = self.hass.data[ASTERISK_DOMAIN]
client = data.client
_LOGGER.info("Deleting: %s", msgid)
await self.hass.async_add_executor_job(client.delete, msgid)
return True

View File

@@ -0,0 +1,9 @@
{
"domain": "asterisk_mbox",
"name": "Asterisk Voicemail",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/asterisk_mbox",
"iot_class": "local_push",
"loggers": ["asterisk_mbox"],
"requirements": ["asterisk_mbox==0.5.0"]
}

View File

@@ -0,0 +1,8 @@
{
"issues": {
"deprecated_integration": {
"title": "The {integration_title} is being removed",
"description": "{integration_title} is being removed as the `{mailbox}` platform is being removed and {integration_title} supports no other platforms. Remove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue."
}
}
}

View File

@@ -52,7 +52,7 @@ SENSORS_TYPE_LOAD_AVG = "sensors_load_avg"
SENSORS_TYPE_RATES = "sensors_rates"
SENSORS_TYPE_TEMPERATURES = "sensors_temperatures"
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"]) # noqa: PYI024
WrtDevice = namedtuple("WrtDevice", ["ip", "name", "connected_to"])
_LOGGER = logging.getLogger(__name__)

View File

@@ -9,7 +9,7 @@ from typing import Any
import aiohttp
import voluptuous as vol
from yalexs.authenticator_common import ValidationResult
from yalexs.const import BRANDS, DEFAULT_BRAND
from yalexs.const import BRANDS_WITHOUT_OAUTH, DEFAULT_BRAND
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -118,7 +118,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS),
): vol.In(BRANDS_WITHOUT_OAUTH),
vol.Required(
CONF_LOGIN_METHOD,
default=self._user_auth_details.get(
@@ -208,7 +208,7 @@ class AugustConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required(
CONF_BRAND,
default=self._user_auth_details.get(CONF_BRAND, DEFAULT_BRAND),
): vol.In(BRANDS),
): vol.In(BRANDS_WITHOUT_OAUTH),
vol.Required(CONF_PASSWORD): str,
}
),

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.1.4", "yalexs-ble==2.4.3"]
"requirements": ["yalexs==8.4.1", "yalexs-ble==2.4.3"]
}

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import datetime, timedelta
from functools import partial
import socket
import aiohttp
from yalexs.activity import ACTION_DOORBELL_CALL_MISSED, Activity, ActivityType
@@ -25,7 +26,14 @@ def async_create_august_clientsession(hass: HomeAssistant) -> aiohttp.ClientSess
# Create an aiohttp session instead of using the default one since the
# default one is likely to trigger august's WAF if another integration
# is also using Cloudflare
return aiohttp_client.async_create_clientsession(hass)
#
# The family is set to AF_INET because IPv6 keeps coming up as an issue
# see https://github.com/home-assistant/core/issues/97146
#
# When https://github.com/aio-libs/aiohttp/issues/4451 is implemented
# we can allow IPv6 again
#
return aiohttp_client.async_create_clientsession(hass, family=socket.AF_INET)
def retrieve_time_based_activity(

View File

@@ -322,8 +322,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
},
trigger_service_handler,
)
component.async_register_entity_service(SERVICE_TOGGLE, None, "async_toggle")
component.async_register_entity_service(SERVICE_TURN_ON, None, "async_turn_on")
component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle")
component.async_register_entity_service(SERVICE_TURN_ON, {}, "async_turn_on")
component.async_register_entity_service(
SERVICE_TURN_OFF,
{vol.Optional(CONF_STOP_ACTIONS, default=DEFAULT_STOP_ACTIONS): cv.boolean},

View File

@@ -68,20 +68,20 @@ class BangOlufsenModel(StrEnum):
class WebsocketNotification(StrEnum):
"""Enum for WebSocket notification types."""
PLAYBACK_ERROR = "playback_error"
PLAYBACK_METADATA = "playback_metadata"
PLAYBACK_PROGRESS = "playback_progress"
PLAYBACK_SOURCE = "playback_source"
PLAYBACK_STATE = "playback_state"
SOFTWARE_UPDATE_STATE = "software_update_state"
SOURCE_CHANGE = "source_change"
VOLUME = "volume"
PLAYBACK_ERROR: Final[str] = "playback_error"
PLAYBACK_METADATA: Final[str] = "playback_metadata"
PLAYBACK_PROGRESS: Final[str] = "playback_progress"
PLAYBACK_SOURCE: Final[str] = "playback_source"
PLAYBACK_STATE: Final[str] = "playback_state"
SOFTWARE_UPDATE_STATE: Final[str] = "software_update_state"
SOURCE_CHANGE: Final[str] = "source_change"
VOLUME: Final[str] = "volume"
# Sub-notifications
NOTIFICATION = "notification"
REMOTE_MENU_CHANGED = "remoteMenuChanged"
NOTIFICATION: Final[str] = "notification"
REMOTE_MENU_CHANGED: Final[str] = "remoteMenuChanged"
ALL = "all"
ALL: Final[str] = "all"
DOMAIN: Final[str] = "bang_olufsen"

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
from collections.abc import Callable
import json
import logging
from typing import Any, cast
@@ -138,25 +137,65 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Turn on the dispatchers."""
await self._initialize()
signal_handlers: dict[str, Callable] = {
CONNECTION_STATUS: self._async_update_connection_state,
WebsocketNotification.PLAYBACK_ERROR: self._async_update_playback_error,
WebsocketNotification.PLAYBACK_METADATA: self._async_update_playback_metadata,
WebsocketNotification.PLAYBACK_PROGRESS: self._async_update_playback_progress,
WebsocketNotification.PLAYBACK_STATE: self._async_update_playback_state,
WebsocketNotification.REMOTE_MENU_CHANGED: self._async_update_sources,
WebsocketNotification.SOURCE_CHANGE: self._async_update_source_change,
WebsocketNotification.VOLUME: self._async_update_volume,
}
for signal, signal_handler in signal_handlers.items():
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{signal}",
signal_handler,
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{CONNECTION_STATUS}",
self._async_update_connection_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_ERROR}",
self._async_update_playback_error,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_METADATA}",
self._async_update_playback_metadata,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_PROGRESS}",
self._async_update_playback_progress,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.PLAYBACK_STATE}",
self._async_update_playback_state,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}",
self._async_update_sources,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.SOURCE_CHANGE}",
self._async_update_source_change,
)
)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"{self._unique_id}_{WebsocketNotification.VOLUME}",
self._async_update_volume,
)
)
async def _initialize(self) -> None:
"""Initialize connection dependent variables."""

View File

@@ -9,8 +9,11 @@ from homeassistant.helpers.device_registry import DeviceEntry
from .const import DOMAIN
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
def get_device(hass: HomeAssistant | None, unique_id: str) -> DeviceEntry | None:
"""Get the device."""
if not isinstance(hass, HomeAssistant):
return None
device_registry = dr.async_get(hass)
device = device_registry.async_get_device({(DOMAIN, unique_id)})
assert device

View File

@@ -20,7 +20,6 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum
from .const import (
BANG_OLUFSEN_WEBSOCKET_EVENT,
@@ -93,14 +92,12 @@ class BangOlufsenWebsocket(BangOlufsenBase):
self, notification: WebsocketNotificationTag
) -> None:
"""Send notification dispatch."""
# Try to match the notification type with available WebsocketNotification members
notification_type = try_parse_enum(WebsocketNotification, notification.value)
if notification_type is WebsocketNotification.REMOTE_MENU_CHANGED:
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}",
)
if notification.value:
if WebsocketNotification.REMOTE_MENU_CHANGED in notification.value:
async_dispatcher_send(
self.hass,
f"{self._unique_id}_{WebsocketNotification.REMOTE_MENU_CHANGED}",
)
def on_playback_error_notification(self, notification: PlaybackError) -> None:
"""Send playback_error dispatch."""
@@ -157,6 +154,11 @@ class BangOlufsenWebsocket(BangOlufsenBase):
software_status = await self._client.get_softwareupdate_status()
# Update the HA device if the sw version does not match
if not self._device:
self._device = get_device(self.hass, self._unique_id)
assert self._device
if software_status.software_version != self._device.sw_version:
device_registry = dr.async_get(self.hass)
@@ -167,6 +169,10 @@ class BangOlufsenWebsocket(BangOlufsenBase):
def on_all_notifications_raw(self, notification: dict) -> None:
"""Receive all notifications."""
if not self._device:
self._device = get_device(self.hass, self._unique_id)
assert self._device
# Add the device_id and serial_number to the notification
notification["device_id"] = self._device.id

View File

@@ -39,7 +39,7 @@ def get_scanner(hass: HomeAssistant, config: ConfigType) -> BboxDeviceScanner |
return scanner if scanner.success_init else None
Device = namedtuple("Device", ["mac", "name", "ip", "last_update"]) # noqa: PYI024
Device = namedtuple("Device", ["mac", "name", "ip", "last_update"])
class BboxDeviceScanner(DeviceScanner):

View File

@@ -243,8 +243,8 @@
"power": {
"name": "Power",
"state": {
"off": "[%key:common::state::off%]",
"on": "[%key:common::state::on%]"
"off": "[%key:component::binary_sensor::entity_component::gas::state::off%]",
"on": "[%key:component::binary_sensor::entity_component::gas::state::on%]"
}
},
"presence": {

View File

@@ -6,7 +6,6 @@ from typing import Any
from blebox_uniapi.box import Box
import blebox_uniapi.cover
from blebox_uniapi.cover import BleboxCoverState
from homeassistant.components.cover import (
ATTR_POSITION,
@@ -29,18 +28,19 @@ BLEBOX_TO_COVER_DEVICE_CLASSES = {
"shutter": CoverDeviceClass.SHUTTER,
}
BLEBOX_TO_HASS_COVER_STATES = {
None: None,
# all blebox covers
BleboxCoverState.MOVING_DOWN: STATE_CLOSING,
BleboxCoverState.MOVING_UP: STATE_OPENING,
BleboxCoverState.MANUALLY_STOPPED: STATE_OPEN,
BleboxCoverState.LOWER_LIMIT_REACHED: STATE_CLOSED,
BleboxCoverState.UPPER_LIMIT_REACHED: STATE_OPEN,
# extra states of gateController product
BleboxCoverState.OVERLOAD: STATE_OPEN,
BleboxCoverState.MOTOR_FAILURE: STATE_OPEN,
BleboxCoverState.SAFETY_STOP: STATE_OPEN,
0: STATE_CLOSING, # moving down
1: STATE_OPENING, # moving up
2: STATE_OPEN, # manually stopped
3: STATE_CLOSED, # lower limit
4: STATE_OPEN, # upper limit / open
# gateController
5: STATE_OPEN, # overload
6: STATE_OPEN, # motor failure
# 7 is not used
8: STATE_OPEN, # safety stop
}
@@ -64,20 +64,14 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
"""Initialize a BleBox cover feature."""
super().__init__(feature)
self._attr_device_class = BLEBOX_TO_COVER_DEVICE_CLASSES[feature.device_class]
position = CoverEntityFeature.SET_POSITION if feature.is_slider else 0
stop = CoverEntityFeature.STOP if feature.has_stop else 0
self._attr_supported_features = (
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
position | stop | CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
)
if feature.is_slider:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
if feature.has_stop:
self._attr_supported_features |= CoverEntityFeature.STOP
if feature.has_tilt:
self._attr_supported_features |= (
CoverEntityFeature.SET_TILT_POSITION
| CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
self._attr_supported_features = (
self._attr_supported_features | CoverEntityFeature.SET_TILT_POSITION
)
@property
@@ -111,24 +105,16 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
return self._is_state(STATE_CLOSED)
async def async_open_cover(self, **kwargs: Any) -> None:
"""Fully open the cover position."""
"""Open the cover position."""
await self._feature.async_open()
async def async_close_cover(self, **kwargs: Any) -> None:
"""Fully close the cover position."""
"""Close the cover position."""
await self._feature.async_close()
async def async_open_cover_tilt(self, **kwargs: Any) -> None:
"""Fully open the cover tilt."""
await self._feature.async_set_tilt_position(0)
async def async_close_cover_tilt(self, **kwargs: Any) -> None:
"""Fully close the cover tilt."""
# note: values are reversed
await self._feature.async_set_tilt_position(100)
async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Set the cover position."""
position = kwargs[ATTR_POSITION]
await self._feature.async_set_position(100 - position)
@@ -138,6 +124,7 @@ class BleBoxCoverEntity(BleBoxEntity[blebox_uniapi.cover.Cover], CoverEntity):
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
"""Set the tilt position."""
position = kwargs[ATTR_TILT_POSITION]
await self._feature.async_set_tilt_position(100 - position)

View File

@@ -14,13 +14,13 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
LIGHT_LUX,
PERCENTAGE,
POWER_VOLT_AMPERE_REACTIVE,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfPower,
UnitOfReactivePower,
UnitOfSpeed,
UnitOfTemperature,
)
@@ -85,7 +85,7 @@ SENSOR_TYPES = (
SensorEntityDescription(
key="reactivePower",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfReactivePower.VOLT_AMPERE_REACTIVE,
native_unit_of_measurement=POWER_VOLT_AMPERE_REACTIVE,
),
SensorEntityDescription(
key="activePower",

View File

@@ -51,8 +51,8 @@ async def async_setup_entry(
async_add_entities(entities)
platform = entity_platform.async_get_current_platform()
platform.async_register_entity_service(SERVICE_RECORD, None, "record")
platform.async_register_entity_service(SERVICE_TRIGGER, None, "trigger_camera")
platform.async_register_entity_service(SERVICE_RECORD, {}, "record")
platform.async_register_entity_service(SERVICE_TRIGGER, {}, "trigger_camera")
platform.async_register_entity_service(
SERVICE_SAVE_RECENT_CLIPS,
{vol.Required(CONF_FILE_PATH): cv.string},

View File

@@ -19,7 +19,7 @@
"bluetooth-adapters==0.19.4",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.4",
"dbus-fast==2.23.0",
"habluetooth==3.3.2"
"dbus-fast==2.22.1",
"habluetooth==3.1.3"
]
}

View File

@@ -23,6 +23,8 @@ from .coordinator import BMWDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
SERVICE_SCHEMA = vol.Schema(
vol.Any(
{vol.Required(ATTR_VIN): cv.string},

View File

@@ -52,7 +52,7 @@ async def async_setup_entry(
for service in ENTITY_SERVICES:
platform.async_register_entity_service(
service,
None,
{},
f"async_{service}",
)

View File

@@ -22,6 +22,7 @@ from homeassistant.components.todo import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.config_validation import make_entity_service_schema
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -61,12 +62,14 @@ async def async_setup_entry(
platform.async_register_entity_service(
SERVICE_PUSH_NOTIFICATION,
{
vol.Required(ATTR_NOTIFICATION_TYPE): vol.All(
vol.Upper, cv.enum(BringNotificationType)
),
vol.Optional(ATTR_ITEM_NAME): cv.string,
},
make_entity_service_schema(
{
vol.Required(ATTR_NOTIFICATION_TYPE): vol.All(
vol.Upper, cv.enum(BringNotificationType)
),
vol.Optional(ATTR_ITEM_NAME): cv.string,
}
),
"async_send_message",
)

View File

@@ -9,7 +9,7 @@
"iot_class": "local_polling",
"loggers": ["brother", "pyasn1", "pysmi", "pysnmp"],
"quality_scale": "platinum",
"requirements": ["brother==4.3.0"],
"requirements": ["brother==4.2.0"],
"zeroconf": [
{
"type": "_printer._tcp.local.",

View File

@@ -2,7 +2,7 @@
import dataclasses
from bsblan import BSBLAN, BSBLANConfig, Device, Info, StaticState
from bsblan import BSBLAN, Device, Info, StaticState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -22,7 +22,7 @@ PLATFORMS = [Platform.CLIMATE]
@dataclasses.dataclass
class BSBLanData:
class HomeAssistantBSBLANData:
"""BSBLan data stored in the Home Assistant data object."""
coordinator: BSBLanUpdateCoordinator
@@ -35,29 +35,23 @@ class BSBLanData:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up BSB-Lan from a config entry."""
# create config using BSBLANConfig
config = BSBLANConfig(
host=entry.data[CONF_HOST],
session = async_get_clientsession(hass)
bsblan = BSBLAN(
entry.data[CONF_HOST],
passkey=entry.data[CONF_PASSKEY],
port=entry.data[CONF_PORT],
username=entry.data.get(CONF_USERNAME),
password=entry.data.get(CONF_PASSWORD),
session=session,
)
# create BSBLAN client
session = async_get_clientsession(hass)
bsblan = BSBLAN(config, session)
# Create and perform first refresh of the coordinator
coordinator = BSBLanUpdateCoordinator(hass, entry, bsblan)
await coordinator.async_config_entry_first_refresh()
# Fetch all required data concurrently
device = await bsblan.device()
info = await bsblan.info()
static = await bsblan.static_values()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = BSBLanData(
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantBSBLANData(
client=bsblan,
coordinator=coordinator,
device=device,

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from bsblan import BSBLANError
from bsblan import BSBLAN, BSBLANError, Device, Info, State, StaticState
from homeassistant.components.climate import (
ATTR_HVAC_MODE,
@@ -21,11 +21,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util.enum import try_parse_enum
from . import BSBLanData
from . import HomeAssistantBSBLANData
from .const import ATTR_TARGET_TEMPERATURE, DOMAIN
from .entity import BSBLanEntity
from .entity import BSBLANEntity
PARALLEL_UPDATES = 1
@@ -47,17 +51,24 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up BSBLAN device based on a config entry."""
data: BSBLanData = hass.data[DOMAIN][entry.entry_id]
data: HomeAssistantBSBLANData = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
[
BSBLANClimate(
data,
data.coordinator,
data.client,
data.device,
data.info,
data.static,
entry,
)
]
)
class BSBLANClimate(BSBLanEntity, ClimateEntity):
class BSBLANClimate(
BSBLANEntity, CoordinatorEntity[DataUpdateCoordinator[State]], ClimateEntity
):
"""Defines a BSBLAN climate device."""
_attr_has_entity_name = True
@@ -69,22 +80,30 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_preset_modes = PRESET_MODES
# Determine hvac modes
_attr_hvac_modes = HVAC_MODES
_enable_turn_on_off_backwards_compatibility = False
def __init__(
self,
data: BSBLanData,
coordinator: DataUpdateCoordinator[State],
client: BSBLAN,
device: Device,
info: Info,
static: StaticState,
entry: ConfigEntry,
) -> None:
"""Initialize BSBLAN climate device."""
super().__init__(data.coordinator, data)
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
super().__init__(client, device, info, static, entry)
CoordinatorEntity.__init__(self, coordinator)
self._attr_unique_id = f"{format_mac(device.MAC)}-climate"
self._attr_min_temp = float(data.static.min_temp.value)
self._attr_max_temp = float(data.static.max_temp.value)
if data.static.min_temp.unit in ("&deg;C", "°C"):
self._attr_min_temp = float(static.min_temp.value)
self._attr_max_temp = float(static.max_temp.value)
# check if self.coordinator.data.current_temperature.unit is "&deg;C" or "°C"
if self.coordinator.data.current_temperature.unit in ("&deg;C", "°C"):
self._attr_temperature_unit = UnitOfTemperature.CELSIUS
else:
self._attr_temperature_unit = UnitOfTemperature.FAHRENHEIT
@@ -92,30 +111,30 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if self.coordinator.data.state.current_temperature.value == "---":
if self.coordinator.data.current_temperature.value == "---":
# device returns no current temperature
return None
return float(self.coordinator.data.state.current_temperature.value)
return float(self.coordinator.data.current_temperature.value)
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
return float(self.coordinator.data.state.target_temperature.value)
return float(self.coordinator.data.target_temperature.value)
@property
def hvac_mode(self) -> HVACMode | None:
"""Return hvac operation ie. heat, cool mode."""
if self.coordinator.data.state.hvac_mode.value == PRESET_ECO:
if self.coordinator.data.hvac_mode.value == PRESET_ECO:
return HVACMode.AUTO
return try_parse_enum(HVACMode, self.coordinator.data.state.hvac_mode.value)
return try_parse_enum(HVACMode, self.coordinator.data.hvac_mode.value)
@property
def preset_mode(self) -> str | None:
"""Return the current preset mode."""
if (
self.hvac_mode == HVACMode.AUTO
and self.coordinator.data.state.hvac_mode.value == PRESET_ECO
and self.coordinator.data.hvac_mode.value == PRESET_ECO
):
return PRESET_ECO
return PRESET_NONE
@@ -154,7 +173,7 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
else:
data[ATTR_HVAC_MODE] = kwargs[ATTR_PRESET_MODE]
try:
await self.coordinator.client.thermostat(**data)
await self.client.thermostat(**data)
except BSBLANError as err:
raise HomeAssistantError(
"An error occurred while updating the BSBLAN device",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from bsblan import BSBLAN, BSBLANConfig, BSBLANError
from bsblan import BSBLAN, BSBLANError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -80,15 +80,15 @@ class BSBLANFlowHandler(ConfigFlow, domain=DOMAIN):
async def _get_bsblan_info(self, raise_on_progress: bool = True) -> None:
"""Get device information from an BSBLAN device."""
config = BSBLANConfig(
session = async_get_clientsession(self.hass)
bsblan = BSBLAN(
host=self.host,
passkey=self.passkey,
port=self.port,
username=self.username,
password=self.password,
passkey=self.passkey,
port=self.port,
session=session,
)
session = async_get_clientsession(self.hass)
bsblan = BSBLAN(config, session)
device = await bsblan.device()
self.mac = device.MAC

View File

@@ -21,4 +21,6 @@ ATTR_OUTSIDE_TEMPERATURE: Final = "outside_temperature"
CONF_PASSKEY: Final = "passkey"
CONF_DEVICE_IDENT: Final = "RVS21.831F/127"
DEFAULT_PORT: Final = 80

View File

@@ -1,10 +1,12 @@
"""DataUpdateCoordinator for the BSB-Lan integration."""
from dataclasses import dataclass
from __future__ import annotations
from datetime import timedelta
from random import randint
from bsblan import BSBLAN, BSBLANConnectionError, State
from bsblan import BSBLAN, BSBLANConnectionError
from bsblan.models import State
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
@@ -14,14 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@dataclass
class BSBLanCoordinatorData:
"""BSBLan data stored in the Home Assistant data object."""
state: State
class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
class BSBLanUpdateCoordinator(DataUpdateCoordinator[State]):
"""The BSB-Lan update coordinator."""
config_entry: ConfigEntry
@@ -33,32 +28,30 @@ class BSBLanUpdateCoordinator(DataUpdateCoordinator[BSBLanCoordinatorData]):
client: BSBLAN,
) -> None:
"""Initialize the BSB-Lan coordinator."""
super().__init__(
hass,
logger=LOGGER,
name=f"{DOMAIN}_{config_entry.data[CONF_HOST]}",
update_interval=self._get_update_interval(),
)
self.client = client
def _get_update_interval(self) -> timedelta:
"""Get the update interval with a random offset.
super().__init__(
hass,
LOGGER,
name=f"{DOMAIN}_{config_entry.data[CONF_HOST]}",
# use the default scan interval and add a random number of seconds to avoid timeouts when
# the BSB-Lan device is already/still busy retrieving data,
# e.g. for MQTT or internal logging.
update_interval=SCAN_INTERVAL + timedelta(seconds=randint(1, 8)),
)
Use the default scan interval and add a random number of seconds to avoid timeouts when
the BSB-Lan device is already/still busy retrieving data,
e.g. for MQTT or internal logging.
"""
return SCAN_INTERVAL + timedelta(seconds=randint(1, 8))
async def _async_update_data(self) -> State:
"""Get state from BSB-Lan device."""
# use the default scan interval and add a random number of seconds to avoid timeouts when
# the BSB-Lan device is already/still busy retrieving data, e.g. for MQTT or internal logging.
self.update_interval = SCAN_INTERVAL + timedelta(seconds=randint(1, 8))
async def _async_update_data(self) -> BSBLanCoordinatorData:
"""Get state and sensor data from BSB-Lan device."""
try:
state = await self.client.state()
return await self.client.state()
except BSBLANConnectionError as err:
host = self.config_entry.data[CONF_HOST] if self.config_entry else "unknown"
raise UpdateFailed(
f"Error while establishing connection with BSB-Lan device at {host}"
f"Error while establishing connection with "
f"BSB-Lan device at {self.config_entry.data[CONF_HOST]}"
) from err
self.update_interval = self._get_update_interval()
return BSBLanCoordinatorData(state=state)

View File

@@ -7,7 +7,7 @@ from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import BSBLanData
from . import HomeAssistantBSBLANData
from .const import DOMAIN
@@ -15,10 +15,9 @@ async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: BSBLanData = hass.data[DOMAIN][entry.entry_id]
data: HomeAssistantBSBLANData = hass.data[DOMAIN][entry.entry_id]
return {
"info": data.info.to_dict(),
"device": data.device.to_dict(),
"state": data.coordinator.data.state.to_dict(),
"info": data.info.dict(),
"device": data.device.dict(),
"state": data.coordinator.data.dict(),
}

View File

@@ -1,35 +1,41 @@
"""BSBLan base entity."""
"""Base entity for the BSBLAN integration."""
from __future__ import annotations
from bsblan import BSBLAN, Device, Info, StaticState
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import Entity
from . import BSBLanData
from .const import DOMAIN
from .coordinator import BSBLanUpdateCoordinator
class BSBLanEntity(CoordinatorEntity[BSBLanUpdateCoordinator]):
"""Defines a base BSBLan entity."""
class BSBLANEntity(Entity):
"""Defines a BSBLAN entity."""
_attr_has_entity_name = True
def __init__(
self,
client: BSBLAN,
device: Device,
info: Info,
static: StaticState,
entry: ConfigEntry,
) -> None:
"""Initialize an BSBLAN entity."""
self.client = client
def __init__(self, coordinator: BSBLanUpdateCoordinator, data: BSBLanData) -> None:
"""Initialize BSBLan entity."""
super().__init__(coordinator, data)
host = self.coordinator.config_entry.data["host"]
mac = self.coordinator.config_entry.data["mac"]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, data.device.MAC)},
connections={(CONNECTION_NETWORK_MAC, format_mac(mac))},
name=data.device.name,
connections={(CONNECTION_NETWORK_MAC, format_mac(device.MAC))},
identifiers={(DOMAIN, format_mac(device.MAC))},
manufacturer="BSBLAN Inc.",
model=data.info.device_identification.value,
sw_version=data.device.version,
configuration_url=f"http://{host}",
model=info.device_identification.value,
name=device.name,
sw_version=f"{device.version})",
configuration_url=f"http://{entry.data[CONF_HOST]}",
)

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["bsblan"],
"requirements": ["python-bsblan==0.6.2"]
"requirements": ["python-bsblan==0.5.18"]
}

View File

@@ -51,7 +51,7 @@ def _create_device(data):
return _Device(ip_address, mac, host, status, name)
_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"]) # noqa: PYI024
_Device = namedtuple("_Device", ["ip_address", "mac", "host", "status", "name"])
class BTSmartHubScanner(DeviceScanner):

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