Compare commits

...

136 Commits

Author SHA1 Message Date
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
Franck Nijhof
86722ba05e 2024.8.0 (#123276) 2024-08-07 20:20:43 +02:00
Franck Nijhof
be4810731a Bump version to 2024.8.0 2024-08-07 19:04:33 +02:00
Franck Nijhof
ac6abb363c Bump version to 2024.8.0b9 2024-08-07 18:24:15 +02:00
Michael Hansen
5367886732 Bump intents to 2024.8.7 (#123295) 2024-08-07 18:24:08 +02:00
Stefan Agner
7a51d4ff62 Drop Matter Microwave Oven Mode select entity (#123294) 2024-08-07 18:24:05 +02:00
ashalita
ef564c537d Revert "Upgrade pycoolmasternet-async to 0.2.0" (#123286) 2024-08-07 18:24:02 +02:00
Franck Nijhof
082290b092 Bump version to 2024.8.0b8 2024-08-07 13:15:23 +02:00
Franck Nijhof
4a212791a2 Update wled to 0.20.1 (#123283) 2024-08-07 13:15:12 +02:00
Brett Adams
6bb55ce79e Add missing application credential to Tesla Fleet (#123271)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
2024-08-07 13:15:04 +02:00
Franck Nijhof
782ff12e6e Bump version to 2024.8.0b7 2024-08-07 11:26:03 +02:00
lunmay
af6f78a784 Fix typo on one of islamic_prayer_times calculation_method option (#123281) 2024-08-07 11:25:55 +02:00
Paulus Schoutsen
db32460f3b Reload conversation entries on update (#123279) 2024-08-07 11:25:52 +02:00
Erwin Douna
270990fe39 Tado change repair issue (#123256) 2024-08-07 11:25:48 +02:00
Franck Nijhof
a10fed9d72 Bump version to 2024.8.0b6 2024-08-07 10:22:39 +02:00
tronikos
cc5699bf08 Fix Google Cloud TTS not respecting config values (#123275) 2024-08-07 10:22:30 +02:00
Jesse Hills
ad674a1c2b Update ESPHome voice assistant pipeline log warning (#123269) 2024-08-07 10:22:27 +02:00
J. Nick Koston
b0269faae4 Allow non-admins to subscribe to newer registry update events (#123267) 2024-08-07 10:22:24 +02:00
starkillerOG
1143efedc5 Bump reolink-aio to 0.9.7 (#123263) 2024-08-07 10:22:21 +02:00
Matthias Alphart
9e75b63925 Update knx-frontend to 2024.8.6.211307 (#123261) 2024-08-07 10:22:18 +02:00
puddly
940327dccf Bump ZHA to 0.0.28 (#123259)
* Bump ZHA to 0.0.28

* Drop redundant radio schema conversion
2024-08-07 10:22:14 +02:00
Steve Repsher
0270026f7c Adapt static resource handler to aiohttp 3.10 (#123166) 2024-08-07 10:22:11 +02:00
Franck Nijhof
b636096ac3 Bump version to 2024.8.0b5 2024-08-06 18:08:19 +02:00
Franck Nijhof
a243ed5b23 Update frontend to 20240806.1 (#123252) 2024-08-06 18:07:49 +02:00
Joost Lekkerkerker
3cf3780587 Bump mficlient to 0.5.0 (#123250) 2024-08-06 18:06:50 +02:00
Robert Resch
3d0a0cf376 Bump deebot-client to 8.3.0 (#123249) 2024-08-06 18:05:00 +02:00
J. Nick Koston
7aae9d9ad3 Fix sense doing blocking I/O in the event loop (#123247) 2024-08-06 18:04:57 +02:00
Franck Nijhof
870bb7efd4 Mark FFmpeg integration as system type (#123241) 2024-08-06 18:04:53 +02:00
Robert Resch
35a6679ae9 Delete mobile_app cloudhook if not logged into the cloud (#123234) 2024-08-06 18:04:49 +02:00
Yehazkel
a09d0117b1 Fix Tami4 device name is None (#123156)
Co-authored-by: Robert Resch <robert@resch.dev>
2024-08-06 18:04:44 +02:00
Franck Nijhof
e9fe98f7f9 Bump version to 2024.8.0b4 2024-08-06 13:22:46 +02:00
Franck Nijhof
5b2e188b52 Mark Google Assistant integration as system type (#123233) 2024-08-06 13:22:03 +02:00
Franck Nijhof
c1953e938d Mark Alexa integration as system type (#123232) 2024-08-06 13:21:59 +02:00
Franck Nijhof
77bcbbcf53 Update frontend to 20240806.0 (#123230) 2024-08-06 12:51:24 +02:00
Joost Lekkerkerker
97587fae08 Bump yt-dlp to 2023.08.06 (#123229) 2024-08-06 12:51:21 +02:00
Matthias Alphart
01b54fe1a9 Update knx-frontend to 2024.8.6.85349 (#123226) 2024-08-06 12:51:17 +02:00
Clifford Roche
f796950493 Update greeclimate to 2.1.0 (#123210) 2024-08-06 12:51:14 +02:00
flopp999
495fd946bc Fix growatt server tlx battery api key (#123191) 2024-08-06 12:51:10 +02:00
Jesse Hills
6af1e25d7e Show project version as sw_version in ESPHome (#123183) 2024-08-06 12:51:07 +02:00
Jesse Hills
6d47a4d7e4 Add support for ESPHome update entities to be checked on demand (#123161) 2024-08-06 12:51:04 +02:00
Petro31
fd5533d719 Fix yamaha legacy receivers (#122985) 2024-08-06 12:50:59 +02:00
Franck Nijhof
d530137bec Bump version to 2024.8.0b3 2024-08-05 21:12:09 +02:00
Franck Nijhof
4f722e864c Mark webhook as a system integration type (#123204) 2024-08-05 21:11:46 +02:00
Franck Nijhof
62d38e786d Mark assist_pipeline as a system integration type (#123202) 2024-08-05 21:10:49 +02:00
Franck Nijhof
859874487e Mark tag to be an entity component (#123200) 2024-08-05 21:09:50 +02:00
Bram Kragten
b16bf29819 Update frontend to 20240805.1 (#123196) 2024-08-05 21:09:46 +02:00
Marius
6b10dbb38c Fix state icon for closed valve entities (#123190) 2024-08-05 21:09:43 +02:00
Joost Lekkerkerker
ea20c4b375 Fix MPD issue creation (#123187) 2024-08-05 21:09:40 +02:00
musapinar
0427aeccb0 Add Matter Leedarson RGBTW Bulb to the transition blocklist (#123182) 2024-08-05 21:09:37 +02:00
Matthias Alphart
4898ba932d Use KNX UI entity platform controller class (#123128) 2024-08-05 21:09:32 +02:00
Franck Nijhof
35a3d2306c Bump version to 2024.8.0b2 2024-08-05 12:22:03 +02:00
Calvin Walton
cdb378066c Add Govee H612B to the Matter transition blocklist (#123163) 2024-08-05 12:21:40 +02:00
Brett Adams
85700fd80f Fix class attribute condition in Tesla Fleet (#123162) 2024-08-05 12:21:37 +02:00
J. Nick Koston
73a2ad7304 Bump aiohttp to 3.10.1 (#123159) 2024-08-05 12:21:34 +02:00
dupondje
f6c4b6b045 dsmr: migrate hourly_gas_meter_reading to mbus device (#123149) 2024-08-05 12:21:30 +02:00
Steve Repsher
0b4d921762 Restore old service worker URL (#123131) 2024-08-05 12:21:27 +02:00
David F. Mulcahey
c8a0e5228d Bump ZHA lib to 0.0.27 (#123125) 2024-08-05 12:21:23 +02:00
Kim de Vos
832bac8c63 Use slugify to create id for UniFi WAN latency (#123108)
Use slugify to create id for latency
2024-08-05 12:21:20 +02:00
Arie Catsman
eccce7017f Bump pyenphase to 1.22.0 (#123103) 2024-08-05 12:21:16 +02:00
Louis Christ
fdb1baadbe Fix wrong DeviceInfo in bluesound integration (#123101)
Fix bluesound device info
2024-08-05 12:21:12 +02:00
Shay Levy
7623ee49e4 Ignore Shelly IPv6 address in zeroconf (#123081) 2024-08-05 12:20:20 +02:00
Mr. Bubbles
fa241dcd04 Catch exception in coordinator setup of IronOS integration (#123079) 2024-08-05 12:20:17 +02:00
Denis Shulyaka
bee77041e8 Change enum type to string for Google Generative AI Conversation (#123069) 2024-08-05 12:20:13 +02:00
Paulus Schoutsen
50b7eb44d1 Add CONTROL supported feature to Google conversation when API access (#123046)
* Add CONTROL supported feature to Google conversation when API access

* Better function name

* Handle entry update inline

* Reload instead of update
2024-08-05 12:20:10 +02:00
Clifford Roche
7b1bf82e3c Update greeclimate to 2.0.0 (#121030)
Co-authored-by: Joostlek <joostlek@outlook.com>
2024-08-05 12:20:01 +02:00
202 changed files with 3092 additions and 1097 deletions

View File

@@ -18,9 +18,12 @@ from homeassistant.const import (
EVENT_THEMES_UPDATED,
)
from homeassistant.helpers.area_registry import EVENT_AREA_REGISTRY_UPDATED
from homeassistant.helpers.category_registry import EVENT_CATEGORY_REGISTRY_UPDATED
from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED
from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED
from homeassistant.helpers.floor_registry import EVENT_FLOOR_REGISTRY_UPDATED
from homeassistant.helpers.issue_registry import EVENT_REPAIRS_ISSUE_REGISTRY_UPDATED
from homeassistant.helpers.label_registry import EVENT_LABEL_REGISTRY_UPDATED
from homeassistant.util.event_type import EventType
# These are events that do not contain any sensitive data
@@ -41,4 +44,7 @@ SUBSCRIBE_ALLOWLIST: Final[set[EventType[Any] | str]] = {
EVENT_SHOPPING_LIST_UPDATED,
EVENT_STATE_CHANGED,
EVENT_THEMES_UPDATED,
EVENT_LABEL_REGISTRY_UPDATED,
EVENT_CATEGORY_REGISTRY_UPDATED,
EVENT_FLOOR_REGISTRY_UPDATED,
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling",
"loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.5.3"]
"requirements": ["AEMET-OpenData==0.5.4"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["airgradient==0.7.1"],
"requirements": ["airgradient==0.8.0"],
"zeroconf": ["_airgradient._tcp.local."]
}

View File

@@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
)
try:
await airzone.get_version()
except AirzoneError as err:
except (AirzoneError, TimeoutError) as err:
raise AbortFlow("cannot_connect") from err
return await self.async_step_discovered_connection()

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.1"]
"requirements": ["aioairzone-cloud==0.6.2"]
}

View File

@@ -5,5 +5,6 @@
"codeowners": ["@home-assistant/cloud", "@ochlocracy", "@jbouwh"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/alexa",
"integration_type": "system",
"iot_class": "cloud_push"
}

View File

@@ -4,6 +4,7 @@
"codeowners": ["@balloob", "@synesthesiam"],
"dependencies": ["conversation", "stt", "tts", "wake_word"],
"documentation": "https://www.home-assistant.io/integrations/assist_pipeline",
"integration_type": "system",
"iot_class": "local_push",
"quality_scale": "internal",
"requirements": ["pymicro-vad==1.0.1"]

View File

@@ -244,7 +244,6 @@ class BluesoundPlayer(MediaPlayerEntity):
self._status: Status | None = None
self._inputs: list[Input] = []
self._presets: list[Preset] = []
self._is_online = False
self._muted = False
self._master: BluesoundPlayer | None = None
self._is_master = False
@@ -255,7 +254,7 @@ class BluesoundPlayer(MediaPlayerEntity):
self._attr_unique_id = format_unique_id(sync_status.mac, port)
# there should always be one player with the default port per mac
if port is DEFAULT_PORT:
if port == DEFAULT_PORT:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
@@ -312,26 +311,33 @@ class BluesoundPlayer(MediaPlayerEntity):
async def _start_poll_command(self):
"""Loop which polls the status of the player."""
try:
while True:
while True:
try:
await self.async_update_status()
except (TimeoutError, ClientError):
_LOGGER.error("Node %s:%s is offline, retrying later", self.name, self.port)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
except CancelledError:
_LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port)
except Exception:
_LOGGER.exception("Unexpected error in %s:%s", self.name, self.port)
raise
except (TimeoutError, ClientError):
_LOGGER.error(
"Node %s:%s is offline, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
except CancelledError:
_LOGGER.debug(
"Stopping the polling of node %s:%s", self.host, self.port
)
return
except Exception:
_LOGGER.exception(
"Unexpected error in %s:%s, retrying later", self.host, self.port
)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
async def async_added_to_hass(self) -> None:
"""Start the polling task."""
await super().async_added_to_hass()
self._polling_task = self.hass.async_create_task(self._start_poll_command())
self._polling_task = self.hass.async_create_background_task(
self._start_poll_command(),
name=f"bluesound.polling_{self.host}:{self.port}",
)
async def async_will_remove_from_hass(self) -> None:
"""Stop the polling task."""
@@ -345,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity):
async def async_update(self) -> None:
"""Update internal status of the entity."""
if not self._is_online:
if not self.available:
return
with suppress(TimeoutError):
@@ -362,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity):
try:
status = await self._player.status(etag=etag, poll_timeout=120, timeout=125)
self._is_online = True
self._attr_available = True
self._last_status_update = dt_util.utcnow()
self._status = status
@@ -391,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity):
self.async_write_ha_state()
except (TimeoutError, ClientError):
self._is_online = False
self._attr_available = False
self._last_status_update = None
self._status = None
self.async_write_ha_state()

View File

@@ -16,7 +16,7 @@
"requirements": [
"bleak==0.22.2",
"bleak-retry-connector==3.5.0",
"bluetooth-adapters==0.19.3",
"bluetooth-adapters==0.19.4",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.4",
"dbus-fast==2.22.1",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
"iot_class": "cloud_push",
"loggers": ["dio_chacon_api"],
"requirements": ["dio-chacon-wifi-api==1.1.0"]
"requirements": ["dio-chacon-wifi-api==1.2.0"]
}

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
from datetime import timedelta
import logging
from coinbase.wallet.client import Client
from coinbase.rest import RESTClient
from coinbase.rest.rest_base import HTTPError
from coinbase.wallet.client import Client as LegacyClient
from coinbase.wallet.error import AuthenticationError
from homeassistant.config_entries import ConfigEntry
@@ -15,8 +17,23 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.util import Throttle
from .const import (
ACCOUNT_IS_VAULT,
API_ACCOUNT_AMOUNT,
API_ACCOUNT_AVALIABLE,
API_ACCOUNT_BALANCE,
API_ACCOUNT_CURRENCY,
API_ACCOUNT_CURRENCY_CODE,
API_ACCOUNT_HOLD,
API_ACCOUNT_ID,
API_ACCOUNTS_DATA,
API_ACCOUNT_NAME,
API_ACCOUNT_VALUE,
API_ACCOUNTS,
API_DATA,
API_RATES_CURRENCY,
API_RESOURCE_TYPE,
API_TYPE_VAULT,
API_V3_ACCOUNT_ID,
API_V3_TYPE_VAULT,
CONF_CURRENCIES,
CONF_EXCHANGE_BASE,
CONF_EXCHANGE_RATES,
@@ -59,9 +76,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData:
"""Create and update a Coinbase Data instance."""
client = Client(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN])
if "organizations" not in entry.data[CONF_API_KEY]:
client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN])
version = "v2"
else:
client = RESTClient(
api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN]
)
version = "v3"
base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD")
instance = CoinbaseData(client, base_rate)
instance = CoinbaseData(client, base_rate, version)
instance.update()
return instance
@@ -86,42 +110,83 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non
registry.async_remove(entity.entity_id)
def get_accounts(client):
def get_accounts(client, version):
"""Handle paginated accounts."""
response = client.get_accounts()
accounts = response[API_ACCOUNTS_DATA]
next_starting_after = response.pagination.next_starting_after
while next_starting_after:
response = client.get_accounts(starting_after=next_starting_after)
accounts += response[API_ACCOUNTS_DATA]
if version == "v2":
accounts = response[API_DATA]
next_starting_after = response.pagination.next_starting_after
return accounts
while next_starting_after:
response = client.get_accounts(starting_after=next_starting_after)
accounts += response[API_DATA]
next_starting_after = response.pagination.next_starting_after
return [
{
API_ACCOUNT_ID: account[API_ACCOUNT_ID],
API_ACCOUNT_NAME: account[API_ACCOUNT_NAME],
API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][
API_ACCOUNT_CURRENCY_CODE
],
API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT],
ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT,
}
for account in accounts
]
accounts = response[API_ACCOUNTS]
while response["has_next"]:
response = client.get_accounts(cursor=response["cursor"])
accounts += response["accounts"]
return [
{
API_ACCOUNT_ID: account[API_V3_ACCOUNT_ID],
API_ACCOUNT_NAME: account[API_ACCOUNT_NAME],
API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY],
API_ACCOUNT_AMOUNT: account[API_ACCOUNT_AVALIABLE][API_ACCOUNT_VALUE]
+ account[API_ACCOUNT_HOLD][API_ACCOUNT_VALUE],
ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_V3_TYPE_VAULT,
}
for account in accounts
]
class CoinbaseData:
"""Get the latest data and update the states."""
def __init__(self, client, exchange_base):
def __init__(self, client, exchange_base, version):
"""Init the coinbase data object."""
self.client = client
self.accounts = None
self.exchange_base = exchange_base
self.exchange_rates = None
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
if version == "v2":
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
else:
self.user_id = (
"v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID]
)
self.api_version = version
@Throttle(MIN_TIME_BETWEEN_UPDATES)
def update(self):
"""Get the latest data from coinbase."""
try:
self.accounts = get_accounts(self.client)
self.exchange_rates = self.client.get_exchange_rates(
currency=self.exchange_base
)
except AuthenticationError as coinbase_error:
self.accounts = get_accounts(self.client, self.api_version)
if self.api_version == "v2":
self.exchange_rates = self.client.get_exchange_rates(
currency=self.exchange_base
)
else:
self.exchange_rates = self.client.get(
"/v2/exchange-rates",
params={API_RATES_CURRENCY: self.exchange_base},
)[API_DATA]
except (AuthenticationError, HTTPError) as coinbase_error:
_LOGGER.error(
"Authentication error connecting to coinbase: %s", coinbase_error
)

View File

@@ -5,7 +5,9 @@ from __future__ import annotations
import logging
from typing import Any
from coinbase.wallet.client import Client
from coinbase.rest import RESTClient
from coinbase.rest.rest_base import HTTPError
from coinbase.wallet.client import Client as LegacyClient
from coinbase.wallet.error import AuthenticationError
import voluptuous as vol
@@ -15,18 +17,17 @@ from homeassistant.config_entries import (
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from . import get_accounts
from .const import (
ACCOUNT_IS_VAULT,
API_ACCOUNT_CURRENCY,
API_ACCOUNT_CURRENCY_CODE,
API_DATA,
API_RATES,
API_RESOURCE_TYPE,
API_TYPE_VAULT,
CONF_CURRENCIES,
CONF_EXCHANGE_BASE,
CONF_EXCHANGE_PRECISION,
@@ -49,8 +50,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
def get_user_from_client(api_key, api_token):
"""Get the user name from Coinbase API credentials."""
client = Client(api_key, api_token)
return client.get_current_user()
if "organizations" not in api_key:
client = LegacyClient(api_key, api_token)
return client.get_current_user()["name"]
client = RESTClient(api_key=api_key, api_secret=api_token)
return client.get_portfolios()["portfolios"][0]["name"]
async def validate_api(hass: HomeAssistant, data):
@@ -60,11 +64,13 @@ async def validate_api(hass: HomeAssistant, data):
user = await hass.async_add_executor_job(
get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN]
)
except AuthenticationError as error:
if "api key" in str(error):
except (AuthenticationError, HTTPError) as error:
if "api key" in str(error) or " 401 Client Error" in str(error):
_LOGGER.debug("Coinbase rejected API credentials due to an invalid API key")
raise InvalidKey from error
if "invalid signature" in str(error):
if "invalid signature" in str(
error
) or "'Could not deserialize key data" in str(error):
_LOGGER.debug(
"Coinbase rejected API credentials due to an invalid API secret"
)
@@ -73,8 +79,8 @@ async def validate_api(hass: HomeAssistant, data):
raise InvalidAuth from error
except ConnectionError as error:
raise CannotConnect from error
return {"title": user["name"]}
api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2"
return {"title": user, "api_version": api_version}
async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options):
@@ -82,14 +88,20 @@ async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, optio
client = hass.data[DOMAIN][config_entry.entry_id].client
accounts = await hass.async_add_executor_job(get_accounts, client)
accounts = await hass.async_add_executor_job(
get_accounts, client, config_entry.data.get("api_version", "v2")
)
accounts_currencies = [
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
account[API_ACCOUNT_CURRENCY]
for account in accounts
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
if not account[ACCOUNT_IS_VAULT]
]
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
if config_entry.data.get("api_version", "v2") == "v2":
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
else:
resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates")
available_rates = resp[API_DATA]
if CONF_CURRENCIES in options:
for currency in options[CONF_CURRENCIES]:
if currency not in accounts_currencies:
@@ -134,6 +146,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
user_input[CONF_API_VERSION] = info["api_version"]
return self.async_create_entry(title=info["title"], data=user_input)
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors

View File

@@ -1,5 +1,7 @@
"""Constants used for Coinbase."""
ACCOUNT_IS_VAULT = "is_vault"
CONF_CURRENCIES = "account_balance_currencies"
CONF_EXCHANGE_BASE = "exchange_base"
CONF_EXCHANGE_RATES = "exchange_rate_currencies"
@@ -10,18 +12,25 @@ DOMAIN = "coinbase"
# Constants for data returned by Coinbase API
API_ACCOUNT_AMOUNT = "amount"
API_ACCOUNT_AVALIABLE = "available_balance"
API_ACCOUNT_BALANCE = "balance"
API_ACCOUNT_CURRENCY = "currency"
API_ACCOUNT_CURRENCY_CODE = "code"
API_ACCOUNT_HOLD = "hold"
API_ACCOUNT_ID = "id"
API_ACCOUNT_NATIVE_BALANCE = "balance"
API_ACCOUNT_NAME = "name"
API_ACCOUNTS_DATA = "data"
API_ACCOUNT_VALUE = "value"
API_ACCOUNTS = "accounts"
API_DATA = "data"
API_RATES = "rates"
API_RATES_CURRENCY = "currency"
API_RESOURCE_PATH = "resource_path"
API_RESOURCE_TYPE = "type"
API_TYPE_VAULT = "vault"
API_USD = "USD"
API_V3_ACCOUNT_ID = "uuid"
API_V3_TYPE_VAULT = "ACCOUNT_TYPE_VAULT"
WALLETS = {
"1INCH": "1INCH",

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/coinbase",
"iot_class": "cloud_polling",
"loggers": ["coinbase"],
"requirements": ["coinbase==2.1.0"]
"requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"]
}

View File

@@ -12,15 +12,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import CoinbaseData
from .const import (
ACCOUNT_IS_VAULT,
API_ACCOUNT_AMOUNT,
API_ACCOUNT_BALANCE,
API_ACCOUNT_CURRENCY,
API_ACCOUNT_CURRENCY_CODE,
API_ACCOUNT_ID,
API_ACCOUNT_NAME,
API_RATES,
API_RESOURCE_TYPE,
API_TYPE_VAULT,
CONF_CURRENCIES,
CONF_EXCHANGE_PRECISION,
CONF_EXCHANGE_PRECISION_DEFAULT,
@@ -31,6 +28,7 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
ATTR_NATIVE_BALANCE = "Balance in native currency"
ATTR_API_VERSION = "API Version"
CURRENCY_ICONS = {
"BTC": "mdi:currency-btc",
@@ -56,9 +54,9 @@ async def async_setup_entry(
entities: list[SensorEntity] = []
provided_currencies: list[str] = [
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
account[API_ACCOUNT_CURRENCY]
for account in instance.accounts
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
if not account[ACCOUNT_IS_VAULT]
]
desired_currencies: list[str] = []
@@ -73,6 +71,11 @@ async def async_setup_entry(
)
for currency in desired_currencies:
_LOGGER.debug(
"Attempting to set up %s account sensor with %s API",
currency,
instance.api_version,
)
if currency not in provided_currencies:
_LOGGER.warning(
(
@@ -85,12 +88,17 @@ async def async_setup_entry(
entities.append(AccountSensor(instance, currency))
if CONF_EXCHANGE_RATES in config_entry.options:
entities.extend(
ExchangeRateSensor(
instance, rate, exchange_base_currency, exchange_precision
for rate in config_entry.options[CONF_EXCHANGE_RATES]:
_LOGGER.debug(
"Attempting to set up %s account sensor with %s API",
rate,
instance.api_version,
)
entities.append(
ExchangeRateSensor(
instance, rate, exchange_base_currency, exchange_precision
)
)
for rate in config_entry.options[CONF_EXCHANGE_RATES]
)
async_add_entities(entities)
@@ -105,26 +113,21 @@ class AccountSensor(SensorEntity):
self._coinbase_data = coinbase_data
self._currency = currency
for account in coinbase_data.accounts:
if (
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
):
if account[API_ACCOUNT_CURRENCY] != currency or account[ACCOUNT_IS_VAULT]:
continue
self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}"
self._attr_unique_id = (
f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}"
f"{account[API_ACCOUNT_CURRENCY]}"
)
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][
API_ACCOUNT_CURRENCY_CODE
]
self._attr_native_value = account[API_ACCOUNT_AMOUNT]
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY]
self._attr_icon = CURRENCY_ICONS.get(
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE],
account[API_ACCOUNT_CURRENCY],
DEFAULT_COIN_ICON,
)
self._native_balance = round(
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
float(account[API_ACCOUNT_AMOUNT])
/ float(coinbase_data.exchange_rates[API_RATES][currency]),
2,
)
@@ -144,21 +147,26 @@ class AccountSensor(SensorEntity):
"""Return the state attributes of the sensor."""
return {
ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}",
ATTR_API_VERSION: self._coinbase_data.api_version,
}
def update(self) -> None:
"""Get the latest state of the sensor."""
_LOGGER.debug(
"Updating %s account sensor with %s API",
self._currency,
self._coinbase_data.api_version,
)
self._coinbase_data.update()
for account in self._coinbase_data.accounts:
if (
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
!= self._currency
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
account[API_ACCOUNT_CURRENCY] != self._currency
or account[ACCOUNT_IS_VAULT]
):
continue
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
self._attr_native_value = account[API_ACCOUNT_AMOUNT]
self._native_balance = round(
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
float(account[API_ACCOUNT_AMOUNT])
/ float(self._coinbase_data.exchange_rates[API_RATES][self._currency]),
2,
)
@@ -202,8 +210,13 @@ class ExchangeRateSensor(SensorEntity):
def update(self) -> None:
"""Get the latest state of the sensor."""
_LOGGER.debug(
"Updating %s rate sensor with %s API",
self._currency,
self._coinbase_data.api_version,
)
self._coinbase_data.update()
self._attr_native_value = round(
1 / float(self._coinbase_data.exchange_rates.rates[self._currency]),
1 / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]),
self._precision,
)

View File

@@ -1,12 +1,11 @@
"""Support for Concord232 alarm control panels."""
# mypy: ignore-errors
from __future__ import annotations
import datetime
import logging
# from concord232 import client as concord232_client
from concord232 import client as concord232_client
import requests
import voluptuous as vol

View File

@@ -1,12 +1,11 @@
"""Support for exposing Concord232 elements as sensors."""
# mypy: ignore-errors
from __future__ import annotations
import datetime
import logging
# from concord232 import client as concord232_client
from concord232 import client as concord232_client
import requests
import voluptuous as vol

View File

@@ -2,9 +2,8 @@
"domain": "concord232",
"name": "Concord232",
"codeowners": [],
"disabled": "This integration is disabled because it uses non-open source code to operate.",
"documentation": "https://www.home-assistant.io/integrations/concord232",
"iot_class": "local_polling",
"loggers": ["concord232", "stevedore"],
"requirements": ["concord232==0.15"]
"requirements": ["concord232==0.15.1"]
}

View File

@@ -1,5 +0,0 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.29"]
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.8.7"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/coolmaster",
"iot_class": "local_polling",
"loggers": ["pycoolmasternet_async"],
"requirements": ["pycoolmasternet-async==0.2.0"]
"requirements": ["pycoolmasternet-async==0.1.5"]
}

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/daikin",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.13.1"],
"requirements": ["pydaikin==2.13.4"],
"zeroconf": ["_dkapi._tcp.local."]
}

View File

@@ -195,7 +195,7 @@ class ConfiguredDoorBird:
title: str | None = data.get("title")
if not title or not title.startswith("Home Assistant"):
continue
event = title.split("(")[1].strip(")")
event = title.partition("(")[2].strip(")")
if input_type := favorite_input_type.get(identifier):
events.append(DoorbirdEvent(event, input_type))
elif input_type := default_event_types.get(event):

View File

@@ -431,39 +431,42 @@ def rename_old_gas_to_mbus(
) -> None:
"""Rename old gas sensor to mbus variant."""
dev_reg = dr.async_get(hass)
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
if device_entry_v1 is not None:
device_id = device_entry_v1.id
for dev_id in (mbus_device_id, entry.entry_id):
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)})
if device_entry_v1 is not None:
device_id = device_entry_v1.id
ent_reg = er.async_get(hass)
entries = er.async_entries_for_device(ent_reg, device_id)
ent_reg = er.async_get(hass)
entries = er.async_entries_for_device(ent_reg, device_id)
for entity in entries:
if entity.unique_id.endswith("belgium_5min_gas_meter_reading"):
try:
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=mbus_device_id,
)
except ValueError:
LOGGER.debug(
"Skip migration of %s because it already exists",
entity.entity_id,
)
else:
LOGGER.debug(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
# Cleanup old device
dev_entities = er.async_entries_for_device(
ent_reg, device_id, include_disabled_entities=True
)
if not dev_entities:
dev_reg.async_remove_device(device_id)
for entity in entries:
if entity.unique_id.endswith(
"belgium_5min_gas_meter_reading"
) or entity.unique_id.endswith("hourly_gas_meter_reading"):
try:
ent_reg.async_update_entity(
entity.entity_id,
new_unique_id=mbus_device_id,
device_id=mbus_device_id,
)
except ValueError:
LOGGER.debug(
"Skip migration of %s because it already exists",
entity.entity_id,
)
else:
LOGGER.debug(
"Migrated entity %s from unique id %s to %s",
entity.entity_id,
entity.unique_id,
mbus_device_id,
)
# Cleanup old device
dev_entities = er.async_entries_for_device(
ent_reg, device_id, include_disabled_entities=True
)
if not dev_entities:
dev_reg.async_remove_device(device_id)
def is_supported_description(

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from functools import partial
import logging
import ssl
from typing import Any, cast
@@ -105,11 +106,14 @@ async def _validate_input(
if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
ssl_context = get_default_no_verify_context()
mqtt_config = create_mqtt_config(
device_id=device_id,
country=country,
override_mqtt_url=mqtt_url,
ssl_context=ssl_context,
mqtt_config = await hass.async_add_executor_job(
partial(
create_mqtt_config,
device_id=device_id,
country=country,
override_mqtt_url=mqtt_url,
ssl_context=ssl_context,
)
)
client = MqttClient(mqtt_config, authenticator)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
from functools import partial
import logging
import ssl
from typing import Any
@@ -64,32 +65,28 @@ class EcovacsController:
if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
ssl_context = get_default_no_verify_context()
self._mqtt = MqttClient(
create_mqtt_config(
device_id=self._device_id,
country=country,
override_mqtt_url=mqtt_url,
ssl_context=ssl_context,
),
self._authenticator,
self._mqtt_config_fn = partial(
create_mqtt_config,
device_id=self._device_id,
country=country,
override_mqtt_url=mqtt_url,
ssl_context=ssl_context,
)
self._mqtt_client: MqttClient | None = None
self._added_legacy_entities: set[str] = set()
async def initialize(self) -> None:
"""Init controller."""
mqtt_config_verfied = False
try:
devices = await self._api_client.get_devices()
credentials = await self._authenticator.authenticate()
for device_config in devices:
if isinstance(device_config, DeviceInfo):
# MQTT device
if not mqtt_config_verfied:
await self._mqtt.verify_config()
mqtt_config_verfied = True
device = Device(device_config, self._authenticator)
await device.initialize(self._mqtt)
mqtt = await self._get_mqtt_client()
await device.initialize(mqtt)
self._devices.append(device)
else:
# Legacy device
@@ -116,7 +113,8 @@ class EcovacsController:
await device.teardown()
for legacy_device in self._legacy_devices:
await self._hass.async_add_executor_job(legacy_device.disconnect)
await self._mqtt.disconnect()
if self._mqtt_client is not None:
await self._mqtt_client.disconnect()
await self._authenticator.teardown()
def add_legacy_entity(self, device: VacBot, component: str) -> None:
@@ -127,6 +125,16 @@ class EcovacsController:
"""Check if legacy entity is added."""
return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities
async def _get_mqtt_client(self) -> MqttClient:
"""Return validated MQTT client."""
if self._mqtt_client is None:
config = await self._hass.async_add_executor_job(self._mqtt_config_fn)
mqtt = MqttClient(config, self._authenticator)
await mqtt.verify_config()
self._mqtt_client = mqtt
return self._mqtt_client
@property
def devices(self) -> list[Device]:
"""Return devices."""

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
"iot_class": "cloud_push",
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
"requirements": ["py-sucks==0.9.10", "deebot-client==8.2.0"]
"requirements": ["py-sucks==0.9.10", "deebot-client==8.3.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.2.5"]
"requirements": ["openwebifpy==4.2.7"]
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/enphase_envoy",
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"requirements": ["pyenphase==1.20.6"],
"requirements": ["pyenphase==1.22.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
from typing import Any
from homeassistant.components.weather import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_CLOUDY,
@@ -190,10 +192,12 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None:
if not (half_days := ec_data.daily_forecasts):
return None
def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast:
def get_day_forecast(
fcst: list[dict[str, Any]],
) -> Forecast:
high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
return {
ATTR_FORECAST_TIME: fcst[0]["timestamp"],
ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(),
ATTR_FORECAST_NATIVE_TEMP: high_temp,
ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(

View File

@@ -346,7 +346,7 @@ class ESPHomeManager:
) -> int | None:
"""Start a voice assistant pipeline."""
if self.voice_assistant_pipeline is not None:
_LOGGER.warning("Voice assistant UDP server was not stopped")
_LOGGER.warning("Previous Voice assistant pipeline was not stopped")
self.voice_assistant_pipeline.stop()
self.voice_assistant_pipeline = None
@@ -654,12 +654,13 @@ def _async_setup_device_registry(
if device_info.manufacturer:
manufacturer = device_info.manufacturer
model = device_info.model
hw_version = None
if device_info.project_name:
project_name = device_info.project_name.split(".")
manufacturer = project_name[0]
model = project_name[1]
hw_version = device_info.project_version
sw_version = (
f"{device_info.project_version} (ESPHome {device_info.esphome_version})"
)
suggested_area = None
if device_info.suggested_area:
@@ -674,7 +675,6 @@ def _async_setup_device_registry(
manufacturer=manufacturer,
model=model,
sw_version=sw_version,
hw_version=hw_version,
suggested_area=suggested_area,
)
return device_entry.id

View File

@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==24.6.2",
"aioesphomeapi==25.0.0",
"esphome-dashboard-api==1.2.3",
"bleak-esphome==1.0.0"
],

View File

@@ -8,6 +8,7 @@ from typing import Any
from aioesphomeapi import (
DeviceInfo as ESPHomeDeviceInfo,
EntityInfo,
UpdateCommand,
UpdateInfo,
UpdateState,
)
@@ -259,9 +260,15 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""Return the title of the update."""
return self._state.title
@convert_api_error_ha_error
async def async_update(self) -> None:
"""Command device to check for update."""
if self.available:
self._client.update_command(key=self._key, command=UpdateCommand.CHECK)
@convert_api_error_ha_error
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Update the current value."""
self._client.update_command(key=self._key, install=True)
"""Command device to install update."""
self._client.update_command(key=self._key, command=UpdateCommand.INSTALL)

View File

@@ -3,5 +3,6 @@
"name": "FFmpeg",
"codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/ffmpeg",
"integration_type": "system",
"requirements": ["ha-ffmpeg==3.2.0"]
}

View File

@@ -653,8 +653,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
entities: list[er.RegistryEntry] = er.async_entries_for_config_entry(
entity_reg, config_entry.entry_id
)
orphan_macs: set[str] = set()
for entity in entities:
entry_mac = entity.unique_id.split("_")[0]
if (
@@ -662,17 +660,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
or "_internet_access" in entity.unique_id
) and entry_mac not in device_hosts:
_LOGGER.info("Removing orphan entity entry %s", entity.entity_id)
orphan_macs.add(entry_mac)
entity_reg.async_remove(entity.entity_id)
device_reg = dr.async_get(self.hass)
orphan_connections = {
(CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in orphan_macs
valid_connections = {
(CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in device_hosts
}
for device in dr.async_entries_for_config_entry(
device_reg, config_entry.entry_id
):
if any(con in device.connections for con in orphan_connections):
if not any(con in device.connections for con in valid_connections):
_LOGGER.debug("Removing obsolete device entry %s", device.name)
device_reg.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id

View File

@@ -398,6 +398,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
static_paths_configs: list[StaticPathConfig] = []
for path, should_cache in (
("service_worker.js", False),
("sw-modern.js", False),
("sw-modern.js.map", False),
("sw-legacy.js", False),

View File

@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240802.0"]
"requirements": ["home-assistant-frontend==20240809.0"]
}

View File

@@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
except exceptions.GlancesApiError as err:
raise UpdateFailed from err
# Update computed values
uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None
uptime: datetime | None = None
up_duration: timedelta | None = None
if up_duration := parse_duration(data.get("uptime")):
if "uptime" in data and (up_duration := parse_duration(data["uptime"])):
uptime = self.data["computed"]["uptime"] if self.data else None
# Update uptime if previous value is None or previous uptime is bigger than
# new uptime (i.e. server restarted)
if (
self.data is None
or self.data["computed"]["uptime_duration"] > up_duration
):
if uptime is None or self.data["computed"]["uptime_duration"] > up_duration:
uptime = utcnow() - up_duration
data["computed"] = {"uptime_duration": up_duration, "uptime": uptime}
return data or {}

View File

@@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
entity_description: GlancesSensorEntityDescription
_attr_has_entity_name = True
_data_valid: bool = False
def __init__(
self,
@@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
@property
def available(self) -> bool:
"""Set sensor unavailable when native value is invalid."""
if super().available:
return (
not self._numeric_state_expected
or isinstance(value := self.native_value, (int, float))
or isinstance(value, str)
and value.isnumeric()
)
return False
return super().available and self._data_valid
@callback
def _handle_coordinator_update(self) -> None:
@@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
def _update_native_value(self) -> None:
"""Update sensor native value from coordinator data."""
data = self.coordinator.data[self.entity_description.type]
if dict_val := data.get(self._sensor_label):
data = self.coordinator.data.get(self.entity_description.type)
if data and (dict_val := data.get(self._sensor_label)):
self._attr_native_value = dict_val.get(self.entity_description.key)
elif self.entity_description.key in data:
elif data and (self.entity_description.key in data):
self._attr_native_value = data.get(self.entity_description.key)
else:
self._attr_native_value = None
self._update_data_valid()
def _update_data_valid(self) -> None:
self._data_valid = self._attr_native_value is not None and (
not self._numeric_state_expected
or isinstance(self._attr_native_value, (int, float))
or isinstance(self._attr_native_value, str)
and self._attr_native_value.isnumeric()
)

View File

@@ -5,5 +5,6 @@
"codeowners": ["@home-assistant/cloud"],
"dependencies": ["http"],
"documentation": "https://www.home-assistant.io/integrations/google_assistant",
"integration_type": "system",
"iot_class": "cloud_push"
}

View File

@@ -59,7 +59,10 @@ def tts_options_schema(
vol.Optional(
CONF_GENDER,
description={"suggested_value": config_options.get(CONF_GENDER)},
default=texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined]
default=config_options.get(
CONF_GENDER,
texttospeech.SsmlVoiceGender.NEUTRAL.name, # type: ignore[attr-defined]
),
): vol.All(
vol.Upper,
SelectSelector(
@@ -72,7 +75,7 @@ def tts_options_schema(
vol.Optional(
CONF_VOICE,
description={"suggested_value": config_options.get(CONF_VOICE)},
default=DEFAULT_VOICE,
default=config_options.get(CONF_VOICE, DEFAULT_VOICE),
): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
@@ -82,7 +85,10 @@ def tts_options_schema(
vol.Optional(
CONF_ENCODING,
description={"suggested_value": config_options.get(CONF_ENCODING)},
default=texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined]
default=config_options.get(
CONF_ENCODING,
texttospeech.AudioEncoding.MP3.name, # type: ignore[attr-defined]
),
): vol.All(
vol.Upper,
SelectSelector(
@@ -95,22 +101,22 @@ def tts_options_schema(
vol.Optional(
CONF_SPEED,
description={"suggested_value": config_options.get(CONF_SPEED)},
default=1.0,
default=config_options.get(CONF_SPEED, 1.0),
): NumberSelector(NumberSelectorConfig(min=0.25, max=4.0, step=0.01)),
vol.Optional(
CONF_PITCH,
description={"suggested_value": config_options.get(CONF_PITCH)},
default=0,
default=config_options.get(CONF_PITCH, 0),
): NumberSelector(NumberSelectorConfig(min=-20.0, max=20.0, step=0.1)),
vol.Optional(
CONF_GAIN,
description={"suggested_value": config_options.get(CONF_GAIN)},
default=0,
default=config_options.get(CONF_GAIN, 0),
): NumberSelector(NumberSelectorConfig(min=-96.0, max=16.0, step=0.1)),
vol.Optional(
CONF_PROFILES,
description={"suggested_value": config_options.get(CONF_PROFILES)},
default=[],
default=config_options.get(CONF_PROFILES, []),
): SelectSelector(
SelectSelectorConfig(
mode=SelectSelectorMode.DROPDOWN,
@@ -132,7 +138,7 @@ def tts_options_schema(
vol.Optional(
CONF_TEXT_TYPE,
description={"suggested_value": config_options.get(CONF_TEXT_TYPE)},
default="text",
default=config_options.get(CONF_TEXT_TYPE, "text"),
): vol.All(
vol.Lower,
SelectSelector(

View File

@@ -89,9 +89,9 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]:
key = "type_"
val = val.upper()
elif key == "format":
if (schema.get("type") == "string" and val != "enum") or (
schema.get("type") not in ("number", "integer", "string")
):
if schema.get("type") == "string" and val != "enum":
continue
if schema.get("type") not in ("number", "integer", "string"):
continue
key = "format_"
elif key == "items":
@@ -100,11 +100,19 @@ def _format_schema(schema: dict[str, Any]) -> dict[str, Any]:
val = {k: _format_schema(v) for k, v in val.items()}
result[key] = val
if result.get("enum") and result.get("type_") != "STRING":
# enum is only allowed for STRING type. This is safe as long as the schema
# contains vol.Coerce for the respective type, for example:
# vol.All(vol.Coerce(int), vol.In([1, 2, 3]))
result["type_"] = "STRING"
result["enum"] = [str(item) for item in result["enum"]]
if result.get("type_") == "OBJECT" and not result.get("properties"):
# An object with undefined properties is not supported by Gemini API.
# Fallback to JSON string. This will probably fail for most tools that want it,
# but we don't have a better fallback strategy so far.
result["properties"] = {"json": {"type_": "STRING"}}
result["required"] = []
return result
@@ -164,6 +172,10 @@ class GoogleGenerativeAIConversationEntity(
model="Generative AI",
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["*"]:
@@ -177,6 +189,9 @@ class GoogleGenerativeAIConversationEntity(
self.hass, "conversation", self.entry.entry_id, self.entity_id
)
conversation.async_set_agent(self.hass, self.entry, self)
self.entry.async_on_unload(
self.entry.add_update_listener(self._async_entry_update_listener)
)
async def async_will_remove_from_hass(self) -> None:
"""When entity will be removed from Home Assistant."""
@@ -397,3 +412,10 @@ class GoogleGenerativeAIConversationEntity(
parts.append(llm_api.api_prompt)
return "\n".join(parts)
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

@@ -18,3 +18,5 @@ FAN_MEDIUM_HIGH = "medium high"
MAX_ERRORS = 2
TARGET_TEMPERATURE_STEP = 1
UPDATE_INTERVAL = 60

View File

@@ -2,16 +2,20 @@
from __future__ import annotations
from datetime import timedelta
from datetime import datetime, timedelta
import logging
from typing import Any
from greeclimate.device import Device, DeviceInfo
from greeclimate.discovery import Discovery, Listener
from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError
from greeclimate.network import Response
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.json import json_dumps
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util.dt import utcnow
from .const import (
COORDINATORS,
@@ -19,12 +23,13 @@ from .const import (
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
MAX_ERRORS,
UPDATE_INTERVAL,
)
_LOGGER = logging.getLogger(__name__)
class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Manages polling for state changes from the device."""
def __init__(self, hass: HomeAssistant, device: Device) -> None:
@@ -34,28 +39,68 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
hass,
_LOGGER,
name=f"{DOMAIN}-{device.device_info.name}",
update_interval=timedelta(seconds=60),
update_interval=timedelta(seconds=UPDATE_INTERVAL),
always_update=False,
)
self.device = device
self._error_count = 0
self.device.add_handler(Response.DATA, self.device_state_updated)
self.device.add_handler(Response.RESULT, self.device_state_updated)
async def _async_update_data(self):
self._error_count: int = 0
self._last_response_time: datetime = utcnow()
self._last_error_time: datetime | None = None
def device_state_updated(self, *args: Any) -> None:
"""Handle device state updates."""
_LOGGER.debug("Device state updated: %s", json_dumps(args))
self._error_count = 0
self._last_response_time = utcnow()
self.async_set_updated_data(self.device.raw_properties)
async def _async_update_data(self) -> dict[str, Any]:
"""Update the state of the device."""
_LOGGER.debug(
"Updating device state: %s, error count: %d", self.name, self._error_count
)
try:
await self.device.update_state()
except DeviceNotBoundError as error:
raise UpdateFailed(f"Device {self.name} is unavailable") from error
raise UpdateFailed(
f"Device {self.name} is unavailable, device is not bound."
) from error
except DeviceTimeoutError as error:
self._error_count += 1
# Under normal conditions GREE units timeout every once in a while
if self.last_update_success and self._error_count >= MAX_ERRORS:
_LOGGER.warning(
"Device is unavailable: %s (%s)",
self.name,
self.device.device_info,
"Device %s is unavailable: %s", self.name, self.device.device_info
)
raise UpdateFailed(f"Device {self.name} is unavailable") from error
raise UpdateFailed(
f"Device {self.name} is unavailable, could not send update request"
) from error
else:
# raise update failed if time for more than MAX_ERRORS has passed since last update
now = utcnow()
elapsed_success = now - self._last_response_time
if self.update_interval and elapsed_success >= self.update_interval:
if not self._last_error_time or (
(now - self.update_interval) >= self._last_error_time
):
self._last_error_time = now
self._error_count += 1
_LOGGER.warning(
"Device %s is unresponsive for %s seconds",
self.name,
elapsed_success,
)
if self.last_update_success and self._error_count >= MAX_ERRORS:
raise UpdateFailed(
f"Device {self.name} is unresponsive for too long and now unavailable"
)
return self.device.raw_properties
async def push_state_update(self):
"""Send state updates to the physical device."""

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==1.4.6"]
"requirements": ["greeclimate==2.1.0"]
}

View File

@@ -22,8 +22,9 @@ from homeassistant.components.notify import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_SERVICE,
CONF_ACTION,
CONF_ENTITIES,
CONF_SERVICE,
STATE_UNAVAILABLE,
)
from homeassistant.core import HomeAssistant, callback
@@ -36,11 +37,37 @@ from .entity import GroupEntity
CONF_SERVICES = "services"
def _backward_compat_schema(value: Any | None) -> Any:
"""Backward compatibility for notify service schemas."""
if not isinstance(value, dict):
return value
# `service` has been renamed to `action`
if CONF_SERVICE in value:
if CONF_ACTION in value:
raise vol.Invalid(
"Cannot specify both 'service' and 'action'. Please use 'action' only."
)
value[CONF_ACTION] = value.pop(CONF_SERVICE)
return value
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_SERVICES): vol.All(
cv.ensure_list,
[{vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict}],
[
vol.All(
_backward_compat_schema,
{
vol.Required(CONF_ACTION): cv.slug,
vol.Optional(ATTR_DATA): dict,
},
)
],
)
}
)
@@ -88,7 +115,7 @@ class GroupNotifyPlatform(BaseNotificationService):
tasks.append(
asyncio.create_task(
self.hass.services.async_call(
DOMAIN, entity[ATTR_SERVICE], sending_payload, blocking=True
DOMAIN, entity[CONF_ACTION], sending_payload, blocking=True
)
)
)

View File

@@ -327,14 +327,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
GrowattSensorEntityDescription(
key="tlx_battery_2_discharge_w",
translation_key="tlx_battery_2_discharge_w",
api_key="bdc1DischargePower",
api_key="bdc2DischargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_discharge_total",
translation_key="tlx_battery_2_discharge_total",
api_key="bdc1DischargeTotal",
api_key="bdc2DischargeTotal",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
@@ -376,14 +376,14 @@ TLX_SENSOR_TYPES: tuple[GrowattSensorEntityDescription, ...] = (
GrowattSensorEntityDescription(
key="tlx_battery_2_charge_w",
translation_key="tlx_battery_2_charge_w",
api_key="bdc1ChargePower",
api_key="bdc2ChargePower",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
),
GrowattSensorEntityDescription(
key="tlx_battery_2_charge_total",
translation_key="tlx_battery_2_charge_total",
api_key="bdc1ChargeTotal",
api_key="bdc2ChargeTotal",
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,

View File

@@ -60,8 +60,11 @@
"integration_not_found": {
"title": "Integration {domain} not found",
"fix_flow": {
"abort": {
"issue_ignored": "Not existing integration {domain} ignored."
},
"step": {
"remove_entries": {
"init": {
"title": "[%key:component::homeassistant::issues::integration_not_found::title%]",
"description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.",
"menu_options": {

View File

@@ -22,6 +22,7 @@ from homeassistant.components import (
sensor,
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
from homeassistant.components.media_player import (
DOMAIN as MEDIA_PLAYER_DOMAIN,
@@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
vol.Optional(
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
): cv.positive_int,
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN),
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(
[binary_sensor.DOMAIN, EVENT_DOMAIN]
),
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
binary_sensor.DOMAIN
[binary_sensor.DOMAIN, EVENT_DOMAIN]
),
}
)

View File

@@ -845,21 +845,41 @@ class HKDevice:
async def async_update(self, now: datetime | None = None) -> None:
"""Poll state of all entities attached to this bridge/accessory."""
to_poll = self.pollable_characteristics
accessories = self.entity_map.accessories
if (
len(self.entity_map.accessories) == 1
len(accessories) == 1
and self.available
and not (self.pollable_characteristics - self.watchable_characteristics)
and not (to_poll - self.watchable_characteristics)
and self.pairing.is_available
and await self.pairing.controller.async_reachable(
self.unique_id, timeout=5.0
)
):
# If its a single accessory and all chars are watchable,
# we don't need to poll.
_LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id)
return
# only poll the firmware version to keep the connection alive
# https://github.com/home-assistant/core/issues/123412
#
# Firmware revision is used here since iOS does this to keep camera
# connections alive, and the goal is to not regress
# https://github.com/home-assistant/core/issues/116143
# by polling characteristics that are not normally polled frequently
# and may not be tested by the device vendor.
#
_LOGGER.debug(
"Accessory is reachable, limiting poll to firmware version: %s",
self.unique_id,
)
first_accessory = accessories[0]
accessory_info = first_accessory.services.first(
service_type=ServicesTypes.ACCESSORY_INFORMATION
)
assert accessory_info is not None
firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid
to_poll = {(first_accessory.aid, firmware_iid)}
if not self.pollable_characteristics:
if not to_poll:
self.async_update_available_state()
_LOGGER.debug(
"HomeKit connection not polling any characteristics: %s", self.unique_id
@@ -892,9 +912,7 @@ class HKDevice:
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)
try:
new_values_dict = await self.get_characteristics(
self.pollable_characteristics
)
new_values_dict = await self.get_characteristics(to_poll)
except AccessoryNotFoundError:
# Not only did the connection fail, but also the accessory is not
# visible on the network.

View File

@@ -14,6 +14,6 @@
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
"iot_class": "local_push",
"loggers": ["aiohomekit", "commentjson"],
"requirements": ["aiohomekit==3.2.1"],
"requirements": ["aiohomekit==3.2.2"],
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
}

View File

@@ -7,6 +7,6 @@
"iot_class": "local_polling",
"loggers": ["homewizard_energy"],
"quality_scale": "platinum",
"requirements": ["python-homewizard-energy==v6.1.1"],
"requirements": ["python-homewizard-energy==v6.2.0"],
"zeroconf": ["_hwenergy._tcp.local."]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homeworks",
"iot_class": "local_push",
"loggers": ["pyhomeworks"],
"requirements": ["pyhomeworks==1.1.0"]
"requirements": ["pyhomeworks==1.1.1"]
}

View File

@@ -533,7 +533,7 @@ class HTML5NotificationService(BaseNotificationService):
elif response.status_code > 399:
_LOGGER.error(
"There was an issue sending the notification %s: %s",
response.status,
response.status_code,
response.text,
)

View File

@@ -3,81 +3,46 @@
from __future__ import annotations
from collections.abc import Mapping
import mimetypes
from pathlib import Path
from typing import Final
from aiohttp import hdrs
from aiohttp.hdrs import CACHE_CONTROL, CONTENT_TYPE
from aiohttp.web import FileResponse, Request, StreamResponse
from aiohttp.web_exceptions import HTTPForbidden, HTTPNotFound
from aiohttp.web_fileresponse import CONTENT_TYPES, FALLBACK_CONTENT_TYPE
from aiohttp.web_urldispatcher import StaticResource
from lru import LRU
from .const import KEY_HASS
CACHE_TIME: Final = 31 * 86400 # = 1 month
CACHE_HEADER = f"public, max-age={CACHE_TIME}"
CACHE_HEADERS: Mapping[str, str] = {hdrs.CACHE_CONTROL: CACHE_HEADER}
PATH_CACHE: LRU[tuple[str, Path], tuple[Path | None, str | None]] = LRU(512)
def _get_file_path(rel_url: str, directory: Path) -> Path | None:
"""Return the path to file on disk or None."""
filename = Path(rel_url)
if filename.anchor:
# rel_url is an absolute name like
# /static/\\machine_name\c$ or /static/D:\path
# where the static dir is totally different
raise HTTPForbidden
filepath: Path = directory.joinpath(filename).resolve()
filepath.relative_to(directory)
# on opening a dir, load its contents if allowed
if filepath.is_dir():
return None
if filepath.is_file():
return filepath
raise FileNotFoundError
CACHE_HEADERS: Mapping[str, str] = {CACHE_CONTROL: CACHE_HEADER}
RESPONSE_CACHE: LRU[tuple[str, Path], tuple[Path, str]] = LRU(512)
class CachingStaticResource(StaticResource):
"""Static Resource handler that will add cache headers."""
async def _handle(self, request: Request) -> StreamResponse:
"""Return requested file from disk as a FileResponse."""
"""Wrap base handler to cache file path resolution and content type guess."""
rel_url = request.match_info["filename"]
key = (rel_url, self._directory)
if (filepath_content_type := PATH_CACHE.get(key)) is None:
hass = request.app[KEY_HASS]
try:
filepath = await hass.async_add_executor_job(_get_file_path, *key)
except (ValueError, FileNotFoundError) as error:
# relatively safe
raise HTTPNotFound from error
except HTTPForbidden:
# forbidden
raise
except Exception as error:
# perm error or other kind!
request.app.logger.exception("Unexpected exception")
raise HTTPNotFound from error
response: StreamResponse
content_type: str | None = None
if filepath is not None:
content_type = (mimetypes.guess_type(rel_url))[
0
] or "application/octet-stream"
PATH_CACHE[key] = (filepath, content_type)
if key in RESPONSE_CACHE:
file_path, content_type = RESPONSE_CACHE[key]
response = FileResponse(file_path, chunk_size=self._chunk_size)
response.headers[CONTENT_TYPE] = content_type
else:
filepath, content_type = filepath_content_type
if filepath and content_type:
return FileResponse(
filepath,
chunk_size=self._chunk_size,
headers={
hdrs.CACHE_CONTROL: CACHE_HEADER,
hdrs.CONTENT_TYPE: content_type,
},
response = await super()._handle(request)
if not isinstance(response, FileResponse):
# Must be directory index; ignore caching
return response
file_path = response._path # noqa: SLF001
response.content_type = (
CONTENT_TYPES.guess_type(file_path)[0] or FALLBACK_CONTENT_TYPE
)
# Cache actual header after setter construction.
content_type = response.headers[CONTENT_TYPE]
RESPONSE_CACHE[key] = (file_path, content_type)
raise HTTPForbidden if filepath is None else HTTPNotFound
response.headers[CACHE_CONTROL] = CACHE_HEADER
return response

View File

@@ -46,4 +46,8 @@ class IronOSCoordinator(DataUpdateCoordinator[LiveDataResponse]):
async def _async_setup(self) -> None:
"""Set up the coordinator."""
self.device_info = await self.device.get_device_info()
try:
self.device_info = await self.device.get_device_info()
except CommunicationError as e:
raise UpdateFailed("Cannot connect to device") from e

View File

@@ -45,7 +45,7 @@
"jakim": "Jabatan Kemajuan Islam Malaysia (JAKIM)",
"tunisia": "Tunisia",
"algeria": "Algeria",
"kemenag": "ementerian Agama Republik Indonesia",
"kemenag": "Kementerian Agama Republik Indonesia",
"morocco": "Morocco",
"portugal": "Comunidade Islamica de Lisboa",
"jordan": "Ministry of Awqaf, Islamic Affairs and Holy Places, Jordan",

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["jvcprojector"],
"requirements": ["pyjvcprojector==1.0.11"]
"requirements": ["pyjvcprojector==1.0.12"]
}

View File

@@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Start the KNX integration."""
hass.data[DATA_HASS_CONFIG] = config
conf: ConfigType | None = config.get(DOMAIN)
if conf is None:
# If we have a config entry, setup is done by that config entry.
# If there is no config entry, this should fail.
return bool(hass.config_entries.async_entries(DOMAIN))
conf = dict(conf)
hass.data[DATA_KNX_CONFIG] = conf
if (conf := config.get(DOMAIN)) is not None:
hass.data[DATA_KNX_CONFIG] = dict(conf)
register_knx_services(hass)
return True

View File

@@ -24,7 +24,7 @@ from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import ATTR_COUNTER, ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import BinarySensorSchema
@@ -43,7 +43,7 @@ async def async_setup_entry(
)
class KNXBinarySensor(KnxEntity, BinarySensorEntity, RestoreEntity):
class KNXBinarySensor(KnxYamlEntity, BinarySensorEntity, RestoreEntity):
"""Representation of a KNX binary sensor."""
_device: XknxBinarySensor

View File

@@ -13,7 +13,7 @@ from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import CONF_PAYLOAD_LENGTH, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_setup_entry(
@@ -31,7 +31,7 @@ async def async_setup_entry(
)
class KNXButton(KnxEntity, ButtonEntity):
class KNXButton(KnxYamlEntity, ButtonEntity):
"""Representation of a KNX button."""
_device: XknxRawValue

View File

@@ -5,7 +5,11 @@ from __future__ import annotations
from typing import Any
from xknx import XKNX
from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
from xknx.devices import (
Climate as XknxClimate,
ClimateMode as XknxClimateMode,
Device as XknxDevice,
)
from xknx.dpt.dpt_20 import HVACControllerMode
from homeassistant import config_entries
@@ -35,7 +39,7 @@ from .const import (
DOMAIN,
PRESET_MODES,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import ClimateSchema
ATTR_COMMAND_VALUE = "command_value"
@@ -133,7 +137,7 @@ def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
)
class KNXClimate(KnxEntity, ClimateEntity):
class KNXClimate(KnxYamlEntity, ClimateEntity):
"""Representation of a KNX climate device."""
_device: XknxClimate
@@ -241,12 +245,9 @@ class KNXClimate(KnxEntity, ClimateEntity):
if self._device.supports_on_off and not self._device.is_on:
return HVACMode.OFF
if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get(
return CONTROLLER_MODES.get(
self._device.mode.controller_mode, self.default_hvac_mode
)
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
return hvac_mode
return self.default_hvac_mode
@property
@@ -261,11 +262,15 @@ class KNXClimate(KnxEntity, ClimateEntity):
if self._device.supports_on_off:
if not ha_controller_modes:
ha_controller_modes.append(self.default_hvac_mode)
ha_controller_modes.append(self._last_hvac_mode)
ha_controller_modes.append(HVACMode.OFF)
hvac_modes = list(set(filter(None, ha_controller_modes)))
return hvac_modes if hvac_modes else [self.default_hvac_mode]
return (
hvac_modes
if hvac_modes
else [self.hvac_mode] # mode read-only -> fall back to only current mode
)
@property
def hvac_action(self) -> HVACAction | None:
@@ -354,3 +359,13 @@ class KNXClimate(KnxEntity, ClimateEntity):
self._device.mode.unregister_device_updated_cb(self.after_update_callback)
self._device.mode.xknx.devices.async_remove(self._device.mode)
await super().async_will_remove_from_hass()
def after_update_callback(self, _device: XknxDevice) -> None:
"""Call after device was updated."""
if self._device.mode is not None and self._device.mode.supports_controller_mode:
hvac_mode = CONTROLLER_MODES.get(
self._device.mode.controller_mode, self.default_hvac_mode
)
if hvac_mode is not HVACMode.OFF:
self._last_hvac_mode = hvac_mode
super().after_update_callback(_device)

View File

@@ -27,7 +27,7 @@ from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import DATA_KNX_CONFIG, DOMAIN
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import CoverSchema
@@ -43,7 +43,7 @@ async def async_setup_entry(
async_add_entities(KNXCover(knx_module, entity_config) for entity_config in config)
class KNXCover(KnxEntity, CoverEntity):
class KNXCover(KnxYamlEntity, CoverEntity):
"""Representation of a KNX cover."""
_device: XknxCover

View File

@@ -31,7 +31,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_setup_entry(
@@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateDevice:
)
class KNXDateEntity(KnxEntity, DateEntity, RestoreEntity):
class KNXDateEntity(KnxYamlEntity, DateEntity, RestoreEntity):
"""Representation of a KNX date."""
_device: XknxDateDevice

View File

@@ -32,7 +32,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_setup_entry(
@@ -62,7 +62,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxDateTimeDevice:
)
class KNXDateTimeEntity(KnxEntity, DateTimeEntity, RestoreEntity):
class KNXDateTimeEntity(KnxYamlEntity, DateTimeEntity, RestoreEntity):
"""Representation of a KNX datetime."""
_device: XknxDateTimeDevice

View File

@@ -21,7 +21,7 @@ from homeassistant.util.scaling import int_states_in_range
from . import KNXModule
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import FanSchema
DEFAULT_PERCENTAGE: Final = 50
@@ -39,7 +39,7 @@ async def async_setup_entry(
async_add_entities(KNXFan(knx_module, entity_config) for entity_config in config)
class KNXFan(KnxEntity, FanEntity):
class KNXFan(KnxYamlEntity, FanEntity):
"""Representation of a KNX fan."""
_device: XknxFan

View File

@@ -2,30 +2,55 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
from xknx.devices import Device as XknxDevice
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.entity_registry import RegistryEntry
if TYPE_CHECKING:
from . import KNXModule
SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal.{{}}"
from .storage.config_store import PlatformControllerBase
class KnxEntity(Entity):
class KnxUiEntityPlatformController(PlatformControllerBase):
"""Class to manage dynamic adding and reloading of UI entities."""
def __init__(
self,
knx_module: KNXModule,
entity_platform: EntityPlatform,
entity_class: type[KnxUiEntity],
) -> None:
"""Initialize the UI platform."""
self._knx_module = knx_module
self._entity_platform = entity_platform
self._entity_class = entity_class
async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None:
"""Add a new UI entity."""
await self._entity_platform.async_add_entities(
[self._entity_class(self._knx_module, unique_id, config)]
)
async def update_entity(
self, entity_entry: RegistryEntry, config: dict[str, Any]
) -> None:
"""Update an existing UI entities configuration."""
await self._entity_platform.async_remove_entity(entity_entry.entity_id)
await self.create_entity(unique_id=entity_entry.unique_id, config=config)
class _KnxEntityBase(Entity):
"""Representation of a KNX entity."""
_attr_should_poll = False
def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None:
"""Set up device."""
self._knx_module = knx_module
self._device = device
_knx_module: KNXModule
_device: XknxDevice
@property
def name(self) -> str:
@@ -49,7 +74,7 @@ class KnxEntity(Entity):
"""Store register state change callback and start device object."""
self._device.register_device_updated_cb(self.after_update_callback)
self._device.xknx.devices.async_add(self._device)
# super call needed to have methods of mulit-inherited classes called
# super call needed to have methods of multi-inherited classes called
# eg. for restoring state (like _KNXSwitch)
await super().async_added_to_hass()
@@ -59,19 +84,22 @@ class KnxEntity(Entity):
self._device.xknx.devices.async_remove(self._device)
class KnxUIEntity(KnxEntity):
class KnxYamlEntity(_KnxEntityBase):
"""Representation of a KNX entity configured from YAML."""
def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None:
"""Initialize the YAML entity."""
self._knx_module = knx_module
self._device = device
class KnxUiEntity(_KnxEntityBase, ABC):
"""Representation of a KNX UI entity."""
_attr_unique_id: str
async def async_added_to_hass(self) -> None:
"""Register callbacks when entity added to hass."""
await super().async_added_to_hass()
self._knx_module.config_store.entities.add(self._attr_unique_id)
self.async_on_remove(
async_dispatcher_connect(
self.hass,
SIGNAL_ENTITY_REMOVE.format(self._attr_unique_id),
self.async_remove,
)
)
@abstractmethod
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize the UI entity."""

View File

@@ -19,15 +19,18 @@ from homeassistant.components.light import (
LightEntity,
)
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.typing import ConfigType
import homeassistant.util.color as color_util
from . import KNXModule
from .const import CONF_SYNC_STATE, DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS, ColorTempModes
from .knx_entity import KnxEntity, KnxUIEntity
from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .schema import LightSchema
from .storage.const import (
CONF_COLOR_TEMP_MAX,
@@ -63,8 +66,17 @@ async def async_setup_entry(
) -> None:
"""Set up light(s) for KNX platform."""
knx_module: KNXModule = hass.data[DOMAIN]
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.LIGHT,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiLight,
),
)
entities: list[KnxEntity] = []
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.LIGHT):
entities.extend(
KnxYamlLight(knx_module, entity_config)
@@ -78,13 +90,6 @@ async def async_setup_entry(
if entities:
async_add_entities(entities)
@callback
def add_new_ui_light(unique_id: str, config: dict[str, Any]) -> None:
"""Add KNX entity at runtime."""
async_add_entities([KnxUiLight(knx_module, unique_id, config)])
knx_module.config_store.async_add_entity[Platform.LIGHT] = add_new_ui_light
def _create_yaml_light(xknx: XKNX, config: ConfigType) -> XknxLight:
"""Return a KNX Light device to be used within XKNX."""
@@ -221,7 +226,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
group_address_color_temp_state = None
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE:
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
group_address_tunable_white_state = [
ga_color_temp[CONF_GA_STATE],
@@ -234,7 +239,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
ga_color_temp[CONF_GA_STATE],
*ga_color_temp[CONF_GA_PASSIVE],
]
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT:
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
_color_dpt = get_dpt(CONF_GA_COLOR)
@@ -519,7 +524,7 @@ class _KnxLight(LightEntity):
await self._device.set_off()
class KnxYamlLight(_KnxLight, KnxEntity):
class KnxYamlLight(_KnxLight, KnxYamlEntity):
"""Representation of a KNX light."""
_device: XknxLight
@@ -546,7 +551,7 @@ class KnxYamlLight(_KnxLight, KnxEntity):
)
class KnxUiLight(_KnxLight, KnxUIEntity):
class KnxUiLight(_KnxLight, KnxUiEntity):
"""Representation of a KNX light."""
_attr_has_entity_name = True
@@ -556,11 +561,9 @@ class KnxUiLight(_KnxLight, KnxUIEntity):
self, knx_module: KNXModule, unique_id: str, config: ConfigType
) -> None:
"""Initialize of KNX light."""
super().__init__(
knx_module=knx_module,
device=_create_ui_light(
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
),
self._knx_module = knx_module
self._device = _create_ui_light(
knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME]
)
self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX]
self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN]

View File

@@ -11,9 +11,9 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
"xknx==3.0.0",
"xknx==3.1.0",
"xknxproject==3.7.1",
"knx-frontend==2024.7.25.204106"
"knx-frontend==2024.8.9.225351"
],
"single_config_entry": true
}

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import KNXModule
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_get_service(
@@ -103,7 +103,7 @@ def _create_notification_instance(xknx: XKNX, config: ConfigType) -> XknxNotific
)
class KNXNotify(KnxEntity, NotifyEntity):
class KNXNotify(KnxYamlEntity, NotifyEntity):
"""Representation of a KNX notification entity."""
_device: XknxNotification

View File

@@ -30,7 +30,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import NumberSchema
@@ -58,7 +58,7 @@ def _create_numeric_value(xknx: XKNX, config: ConfigType) -> NumericValue:
)
class KNXNumber(KnxEntity, RestoreNumber):
class KNXNumber(KnxYamlEntity, RestoreNumber):
"""Representation of a KNX number."""
_device: NumericValue

View File

@@ -15,7 +15,7 @@ from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import DATA_KNX_CONFIG, DOMAIN, KNX_ADDRESS
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import SceneSchema
@@ -31,7 +31,7 @@ async def async_setup_entry(
async_add_entities(KNXScene(knx_module, entity_config) for entity_config in config)
class KNXScene(KnxEntity, Scene):
class KNXScene(KnxYamlEntity, Scene):
"""Representation of a KNX scene."""
_device: XknxScene

View File

@@ -30,7 +30,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import SelectSchema
@@ -59,7 +59,7 @@ def _create_raw_value(xknx: XKNX, config: ConfigType) -> RawValue:
)
class KNXSelect(KnxEntity, SelectEntity, RestoreEntity):
class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity):
"""Representation of a KNX select."""
_device: RawValue

View File

@@ -35,7 +35,7 @@ from homeassistant.util.enum import try_parse_enum
from . import KNXModule
from .const import ATTR_SOURCE, DATA_KNX_CONFIG, DOMAIN
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import SensorSchema
SCAN_INTERVAL = timedelta(seconds=10)
@@ -141,7 +141,7 @@ def _create_sensor(xknx: XKNX, config: ConfigType) -> XknxSensor:
)
class KNXSensor(KnxEntity, SensorEntity):
class KNXSensor(KnxYamlEntity, SensorEntity):
"""Representation of a KNX sensor."""
_device: XknxSensor

View File

@@ -1,6 +1,6 @@
"""KNX entity configuration store."""
from collections.abc import Callable
from abc import ABC, abstractmethod
import logging
from typing import Any, Final, TypedDict
@@ -8,12 +8,10 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PLATFORM, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.storage import Store
from homeassistant.util.ulid import ulid_now
from ..const import DOMAIN
from ..knx_entity import SIGNAL_ENTITY_REMOVE
from .const import CONF_DATA
_LOGGER = logging.getLogger(__name__)
@@ -33,6 +31,20 @@ class KNXConfigStoreModel(TypedDict):
entities: KNXEntityStoreModel
class PlatformControllerBase(ABC):
"""Entity platform controller base class."""
@abstractmethod
async def create_entity(self, unique_id: str, config: dict[str, Any]) -> None:
"""Create a new entity."""
@abstractmethod
async def update_entity(
self, entity_entry: er.RegistryEntry, config: dict[str, Any]
) -> None:
"""Update an existing entities configuration."""
class KNXConfigStore:
"""Manage KNX config store data."""
@@ -46,12 +58,7 @@ class KNXConfigStore:
self.config_entry = config_entry
self._store = Store[KNXConfigStoreModel](hass, STORAGE_VERSION, STORAGE_KEY)
self.data = KNXConfigStoreModel(entities={})
# entities and async_add_entity are filled by platform / entity setups
self.entities: set[str] = set() # unique_id as values
self.async_add_entity: dict[
Platform, Callable[[str, dict[str, Any]], None]
] = {}
self._platform_controllers: dict[Platform, PlatformControllerBase] = {}
async def load_data(self) -> None:
"""Load config store data from storage."""
@@ -62,14 +69,19 @@ class KNXConfigStore:
len(self.data["entities"]),
)
def add_platform(
self, platform: Platform, controller: PlatformControllerBase
) -> None:
"""Add platform controller."""
self._platform_controllers[platform] = controller
async def create_entity(
self, platform: Platform, data: dict[str, Any]
) -> str | None:
"""Create a new entity."""
if platform not in self.async_add_entity:
raise ConfigStoreException(f"Entity platform not ready: {platform}")
platform_controller = self._platform_controllers[platform]
unique_id = f"knx_es_{ulid_now()}"
self.async_add_entity[platform](unique_id, data)
await platform_controller.create_entity(unique_id, data)
# store data after entity was added to be sure config didn't raise exceptions
self.data["entities"].setdefault(platform, {})[unique_id] = data
await self._store.async_save(self.data)
@@ -95,8 +107,7 @@ class KNXConfigStore:
self, platform: Platform, entity_id: str, data: dict[str, Any]
) -> None:
"""Update an existing entity."""
if platform not in self.async_add_entity:
raise ConfigStoreException(f"Entity platform not ready: {platform}")
platform_controller = self._platform_controllers[platform]
entity_registry = er.async_get(self.hass)
if (entry := entity_registry.async_get(entity_id)) is None:
raise ConfigStoreException(f"Entity not found: {entity_id}")
@@ -108,8 +119,7 @@ class KNXConfigStore:
raise ConfigStoreException(
f"Entity not found in storage: {entity_id} - {unique_id}"
)
async_dispatcher_send(self.hass, SIGNAL_ENTITY_REMOVE.format(unique_id))
self.async_add_entity[platform](unique_id, data)
await platform_controller.update_entity(entry, data)
# store data after entity is added to make sure config doesn't raise exceptions
self.data["entities"][platform][unique_id] = data
await self._store.async_save(self.data)
@@ -125,23 +135,21 @@ class KNXConfigStore:
raise ConfigStoreException(
f"Entity not found in {entry.domain}: {entry.unique_id}"
) from err
try:
self.entities.remove(entry.unique_id)
except KeyError:
_LOGGER.warning("Entity not initialized when deleted: %s", entity_id)
entity_registry.async_remove(entity_id)
await self._store.async_save(self.data)
def get_entity_entries(self) -> list[er.RegistryEntry]:
"""Get entity_ids of all configured entities by platform."""
"""Get entity_ids of all UI configured entities."""
entity_registry = er.async_get(self.hass)
unique_ids = {
uid for platform in self.data["entities"].values() for uid in platform
}
return [
registry_entry
for registry_entry in er.async_entries_for_config_entry(
entity_registry, self.config_entry.entry_id
)
if registry_entry.unique_id in self.entities
if registry_entry.unique_id in unique_ids
]

View File

@@ -17,9 +17,12 @@ from homeassistant.const import (
STATE_UNKNOWN,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType
@@ -32,7 +35,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity, KnxUIEntity
from .knx_entity import KnxUiEntity, KnxUiEntityPlatformController, KnxYamlEntity
from .schema import SwitchSchema
from .storage.const import (
CONF_DEVICE_INFO,
@@ -51,8 +54,17 @@ async def async_setup_entry(
) -> None:
"""Set up switch(es) for KNX platform."""
knx_module: KNXModule = hass.data[DOMAIN]
platform = async_get_current_platform()
knx_module.config_store.add_platform(
platform=Platform.SWITCH,
controller=KnxUiEntityPlatformController(
knx_module=knx_module,
entity_platform=platform,
entity_class=KnxUiSwitch,
),
)
entities: list[KnxEntity] = []
entities: list[KnxYamlEntity | KnxUiEntity] = []
if yaml_platform_config := hass.data[DATA_KNX_CONFIG].get(Platform.SWITCH):
entities.extend(
KnxYamlSwitch(knx_module, entity_config)
@@ -66,13 +78,6 @@ async def async_setup_entry(
if entities:
async_add_entities(entities)
@callback
def add_new_ui_switch(unique_id: str, config: dict[str, Any]) -> None:
"""Add KNX entity at runtime."""
async_add_entities([KnxUiSwitch(knx_module, unique_id, config)])
knx_module.config_store.async_add_entity[Platform.SWITCH] = add_new_ui_switch
class _KnxSwitch(SwitchEntity, RestoreEntity):
"""Base class for a KNX switch."""
@@ -102,7 +107,7 @@ class _KnxSwitch(SwitchEntity, RestoreEntity):
await self._device.set_off()
class KnxYamlSwitch(_KnxSwitch, KnxEntity):
class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
"""Representation of a KNX switch configured from YAML."""
_device: XknxSwitch
@@ -125,7 +130,7 @@ class KnxYamlSwitch(_KnxSwitch, KnxEntity):
self._attr_unique_id = str(self._device.switch.group_address)
class KnxUiSwitch(_KnxSwitch, KnxUIEntity):
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
"""Representation of a KNX switch configured from UI."""
_attr_has_entity_name = True
@@ -134,21 +139,19 @@ class KnxUiSwitch(_KnxSwitch, KnxUIEntity):
def __init__(
self, knx_module: KNXModule, unique_id: str, config: dict[str, Any]
) -> None:
"""Initialize of KNX switch."""
super().__init__(
knx_module=knx_module,
device=XknxSwitch(
knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
group_address_state=[
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
],
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN][CONF_INVERT],
),
"""Initialize KNX switch."""
self._knx_module = knx_module
self._device = XknxSwitch(
knx_module.xknx,
name=config[CONF_ENTITY][CONF_NAME],
group_address=config[DOMAIN][CONF_GA_SWITCH][CONF_GA_WRITE],
group_address_state=[
config[DOMAIN][CONF_GA_SWITCH][CONF_GA_STATE],
*config[DOMAIN][CONF_GA_SWITCH][CONF_GA_PASSIVE],
],
respond_to_read=config[DOMAIN][CONF_RESPOND_TO_READ],
sync_state=config[DOMAIN][CONF_SYNC_STATE],
invert=config[DOMAIN][CONF_INVERT],
)
self._attr_entity_category = config[CONF_ENTITY][CONF_ENTITY_CATEGORY]
self._attr_unique_id = unique_id

View File

@@ -30,7 +30,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_setup_entry(
@@ -57,7 +57,7 @@ def _create_notification(xknx: XKNX, config: ConfigType) -> XknxNotification:
)
class KNXText(KnxEntity, TextEntity, RestoreEntity):
class KNXText(KnxYamlEntity, TextEntity, RestoreEntity):
"""Representation of a KNX text."""
_device: XknxNotification

View File

@@ -31,7 +31,7 @@ from .const import (
DOMAIN,
KNX_ADDRESS,
)
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
async def async_setup_entry(
@@ -61,7 +61,7 @@ def _create_xknx_device(xknx: XKNX, config: ConfigType) -> XknxTimeDevice:
)
class KNXTimeEntity(KnxEntity, TimeEntity, RestoreEntity):
class KNXTimeEntity(KnxYamlEntity, TimeEntity, RestoreEntity):
"""Representation of a KNX time."""
_device: XknxTimeDevice

View File

@@ -21,7 +21,7 @@ from homeassistant.helpers.typing import ConfigType
from . import KNXModule
from .const import DATA_KNX_CONFIG, DOMAIN
from .knx_entity import KnxEntity
from .knx_entity import KnxYamlEntity
from .schema import WeatherSchema
@@ -75,7 +75,7 @@ def _create_weather(xknx: XKNX, config: ConfigType) -> XknxWeather:
)
class KNXWeather(KnxEntity, WeatherEntity):
class KNXWeather(KnxYamlEntity, WeatherEntity):
"""Representation of a KNX weather device."""
_device: XknxWeather

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
"iot_class": "cloud_polling",
"loggers": ["lacrosse_view"],
"requirements": ["lacrosse-view==1.0.1"]
"requirements": ["lacrosse-view==1.0.2"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/lcn",
"iot_class": "local_push",
"loggers": ["pypck"],
"requirements": ["pypck==0.7.17"]
"requirements": ["pypck==0.7.20"]
}

View File

@@ -9,7 +9,7 @@
},
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.20.0"],
"requirements": ["pylutron-caseta==0.21.1"],
"zeroconf": [
{
"type": "_lutron._tcp.local.",

View File

@@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity):
@property
def native_value(self) -> float | str | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator)
val = self.entity_description.value_fn(self.coordinator)
# check if sensor is enum
if self.entity_description.device_class == SensorDeviceClass.ENUM:
if (
self.entity_description.options
and val in self.entity_description.options
):
return val
# return None for values that are not in the options
return None
return val

View File

@@ -51,17 +51,19 @@ DEFAULT_TRANSITION = 0.2
# hw version (attributeKey 0/40/8)
# sw version (attributeKey 0/40/10)
TRANSITION_BLOCKLIST = (
(4488, 514, "1.0", "1.0.0"),
(4488, 260, "1.0", "1.0.0"),
(5010, 769, "3.0", "1.0.0"),
(4999, 25057, "1.0", "27.0"),
(4448, 36866, "V1", "V1.0.0.5"),
(5009, 514, "1.0", "1.0.0"),
(4107, 8475, "v1.0", "v1.0"),
(4107, 8550, "v1.0", "v1.0"),
(4107, 8551, "v1.0", "v1.0"),
(4107, 8656, "v1.0", "v1.0"),
(4107, 8571, "v1.0", "v1.0"),
(4107, 8656, "v1.0", "v1.0"),
(4448, 36866, "V1", "V1.0.0.5"),
(4456, 1011, "1.0.0", "2.00.00"),
(4488, 260, "1.0", "1.0.0"),
(4488, 514, "1.0", "1.0.0"),
(4999, 24875, "1.0", "27.0"),
(4999, 25057, "1.0", "27.0"),
(5009, 514, "1.0", "1.0.0"),
(5010, 769, "3.0", "1.0.0"),
)

View File

@@ -27,7 +27,6 @@ type SelectCluster = (
| clusters.RvcRunMode
| clusters.RvcCleanMode
| clusters.DishwasherMode
| clusters.MicrowaveOvenMode
| clusters.EnergyEvseMode
| clusters.DeviceEnergyManagementMode
)
@@ -199,18 +198,6 @@ DISCOVERY_SCHEMAS = [
clusters.DishwasherMode.Attributes.SupportedModes,
),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(
key="MatterMicrowaveOvenMode",
translation_key="mode",
),
entity_class=MatterModeSelectEntity,
required_attributes=(
clusters.MicrowaveOvenMode.Attributes.CurrentMode,
clusters.MicrowaveOvenMode.Attributes.SupportedModes,
),
),
MatterDiscoverySchema(
platform=Platform.SELECT,
entity_description=MatterSelectEntityDescription(

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.8.0"]
"requirements": ["aiomealie==0.8.1"]
}

View File

@@ -8,6 +8,6 @@
"iot_class": "calculated",
"loggers": ["yt_dlp"],
"quality_scale": "internal",
"requirements": ["yt-dlp==2024.07.16"],
"requirements": ["yt-dlp==2024.08.06"],
"single_config_entry": true
}

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/mfi",
"iot_class": "local_polling",
"loggers": ["mficlient"],
"requirements": ["mficlient==0.3.0"]
"requirements": ["mficlient==0.5.0"]
}

View File

@@ -124,12 +124,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
):
await async_create_cloud_hook(hass, webhook_id, entry)
if (
CONF_CLOUDHOOK_URL not in entry.data
and cloud.async_active_subscription(hass)
and cloud.async_is_connected(hass)
):
await async_create_cloud_hook(hass, webhook_id, entry)
if cloud.async_is_logged_in(hass):
if (
CONF_CLOUDHOOK_URL not in entry.data
and cloud.async_active_subscription(hass)
and cloud.async_is_connected(hass)
):
await async_create_cloud_hook(hass, webhook_id, entry)
elif CONF_CLOUDHOOK_URL in entry.data:
# If we have a cloudhook but no longer logged in to the cloud, remove it from the entry
data = dict(entry.data)
data.pop(CONF_CLOUDHOOK_URL)
hass.config_entries.async_update_entry(entry, data=data)
entry.async_on_unload(cloud.async_listen_connection_change(hass, manage_cloudhook))

View File

@@ -6,5 +6,5 @@
"dependencies": ["application_credentials"],
"documentation": "https://www.home-assistant.io/integrations/monzo",
"iot_class": "cloud_polling",
"requirements": ["monzopy==1.3.0"]
"requirements": ["monzopy==1.3.2"]
}

View File

@@ -86,7 +86,7 @@ async def async_setup_platform(
)
if (
result["type"] is FlowResultType.CREATE_ENTRY
or result["reason"] == "single_instance_allowed"
or result["reason"] == "already_configured"
):
async_create_issue(
hass,

View File

@@ -20,5 +20,5 @@
"iot_class": "cloud_push",
"loggers": ["google_nest_sdm"],
"quality_scale": "platinum",
"requirements": ["google-nest-sdm==4.0.5"]
"requirements": ["google-nest-sdm==4.0.6"]
}

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling",
"loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==2.0.3"]
"requirements": ["py-nextbusnext==2.0.4"]
}

View File

@@ -346,9 +346,5 @@ class OllamaConversationEntity(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
if entry.options.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
else:
self._attr_supported_features = conversation.ConversationEntityFeature(0)
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -328,9 +328,5 @@ class OpenAIConversationEntity(
self, hass: HomeAssistant, entry: ConfigEntry
) -> None:
"""Handle options update."""
if entry.options.get(CONF_LLM_HASS_API):
self._attr_supported_features = (
conversation.ConversationEntityFeature.CONTROL
)
else:
self._attr_supported_features = conversation.ConversationEntityFeature(0)
# Reload as we update device info + entity name + supported features
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
from pyopenweathermap import OWMClient
from pyopenweathermap import create_owm_client
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -33,6 +33,7 @@ class OpenweathermapData:
"""Runtime data definition."""
name: str
mode: str
coordinator: WeatherUpdateCoordinator
@@ -52,7 +53,7 @@ async def async_setup_entry(
else:
async_delete_issue(hass, entry.entry_id)
owm_client = OWMClient(api_key, mode, lang=language)
owm_client = create_owm_client(api_key, mode, lang=language)
weather_coordinator = WeatherUpdateCoordinator(
owm_client, latitude, longitude, hass
)
@@ -61,7 +62,7 @@ async def async_setup_entry(
entry.async_on_unload(entry.add_update_listener(async_update_options))
entry.runtime_data = OpenweathermapData(name, weather_coordinator)
entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -58,10 +58,17 @@ FORECAST_MODE_DAILY = "daily"
FORECAST_MODE_FREE_DAILY = "freedaily"
FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly"
FORECAST_MODE_ONECALL_DAILY = "onecall_daily"
OWM_MODE_V25 = "v2.5"
OWM_MODE_FREE_CURRENT = "current"
OWM_MODE_FREE_FORECAST = "forecast"
OWM_MODE_V30 = "v3.0"
OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25]
DEFAULT_OWM_MODE = OWM_MODE_V30
OWM_MODE_V25 = "v2.5"
OWM_MODES = [
OWM_MODE_FREE_CURRENT,
OWM_MODE_FREE_FORECAST,
OWM_MODE_V30,
OWM_MODE_V25,
]
DEFAULT_OWM_MODE = OWM_MODE_FREE_CURRENT
LANGUAGES = [
"af",

View File

@@ -86,8 +86,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
"""Format the weather response correctly."""
_LOGGER.debug("OWM weather response: %s", weather_report)
current_weather = (
self._get_current_weather_data(weather_report.current)
if weather_report.current is not None
else {}
)
return {
ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current),
ATTR_API_CURRENT: current_weather,
ATTR_API_HOURLY_FORECAST: [
self._get_hourly_forecast_weather_data(item)
for item in weather_report.hourly_forecast
@@ -122,6 +128,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
}
def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast):
uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None
return Forecast(
datetime=forecast.date_time.isoformat(),
condition=self._get_condition(forecast.condition.id),
@@ -134,12 +142,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
wind_speed=forecast.wind_speed,
native_wind_gust_speed=forecast.wind_gust,
wind_bearing=forecast.wind_bearing,
uv_index=float(forecast.uv_index),
uv_index=uv_index,
precipitation_probability=round(forecast.precipitation_probability * 100),
precipitation=self._calc_precipitation(forecast.rain, forecast.snow),
)
def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast):
uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None
return Forecast(
datetime=forecast.date_time.isoformat(),
condition=self._get_condition(forecast.condition.id),
@@ -153,7 +163,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
wind_speed=forecast.wind_speed,
native_wind_gust_speed=forecast.wind_gust,
wind_bearing=forecast.wind_bearing,
uv_index=float(forecast.uv_index),
uv_index=uv_index,
precipitation_probability=round(forecast.precipitation_probability * 100),
precipitation=round(forecast.rain + forecast.snow, 2),
)

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
"iot_class": "cloud_polling",
"loggers": ["pyopenweathermap"],
"requirements": ["pyopenweathermap==0.0.9"]
"requirements": ["pyopenweathermap==0.1.1"]
}

View File

@@ -19,6 +19,7 @@ from homeassistant.const import (
UnitOfVolumetricFlux,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@@ -47,6 +48,7 @@ from .const import (
DEFAULT_NAME,
DOMAIN,
MANUFACTURER,
OWM_MODE_FREE_FORECAST,
)
from .coordinator import WeatherUpdateCoordinator
@@ -161,16 +163,23 @@ async def async_setup_entry(
name = domain_data.name
weather_coordinator = domain_data.coordinator
entities: list[AbstractOpenWeatherMapSensor] = [
OpenWeatherMapSensor(
name,
f"{config_entry.unique_id}-{description.key}",
description,
weather_coordinator,
if domain_data.mode == OWM_MODE_FREE_FORECAST:
entity_registry = er.async_get(hass)
entries = er.async_entries_for_config_entry(
entity_registry, config_entry.entry_id
)
for entry in entries:
entity_registry.async_remove(entry.entity_id)
else:
async_add_entities(
OpenWeatherMapSensor(
name,
f"{config_entry.unique_id}-{description.key}",
description,
weather_coordinator,
)
for description in WEATHER_SENSOR_TYPES
)
for description in WEATHER_SENSOR_TYPES
]
async_add_entities(entities)
class AbstractOpenWeatherMapSensor(SensorEntity):

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