Compare commits

...

98 Commits

Author SHA1 Message Date
Franck Nijhof 14af3661f3 Bump version to 2024.7.0b6 2024-06-30 20:42:10 +02:00
Michael af733425c2 Bump pyfritzhome to 0.6.12 (#120861) 2024-06-30 20:41:51 +02:00
Allen Porter 4fc89e8861 Rollback PyFlume to 0.6.5 (#120846) 2024-06-30 20:39:47 +02:00
Tsvi Mostovicz bcec268c04 Fix Jewish calendar unique id move to entity (#120842) 2024-06-30 20:39:44 +02:00
Shay Levy becf9fcce2 Bump aiowebostv to 0.4.1 (#120838) 2024-06-30 20:39:41 +02:00
Etienne Soufflet ad9e0ef8e4 Fix Tado fan mode (#120809) 2024-06-30 20:39:38 +02:00
Simon Lamon f58eafe8fc Fix routes with transfer in nmbs integration (#120808) 2024-06-30 20:39:35 +02:00
mkmer a7246400b3 Allow EM heat on from any mode in Honeywell (#120750) 2024-06-30 20:39:32 +02:00
Joost Lekkerkerker 38a30b343d Bump pizzapi to 0.0.6 (#120691) 2024-06-30 20:39:28 +02:00
Franck Nijhof 08a0eaf184 Bump version to 2024.7.0b5 2024-06-29 17:51:45 +02:00
Joost Lekkerkerker 3ee8f6edba Use meal note as fallback in Mealie (#120828) 2024-06-29 17:51:34 +02:00
Joost Lekkerkerker e866417c01 Add icons to Airgradient (#120820) 2024-06-29 17:51:31 +02:00
Joost Lekkerkerker 05c63eb884 Bump python-opensky to 1.0.1 (#120818) 2024-06-29 17:51:28 +02:00
Joost Lekkerkerker bb52bfd73d Add unique id to Mealie config entry (#120816) 2024-06-29 17:51:25 +02:00
Joost Lekkerkerker 7319492bf3 Bump aiomealie to 0.5.0 (#120815) 2024-06-29 17:51:21 +02:00
J. Nick Koston 66932e3d9a Fix unneeded dict values for MATCH_ALL recorder attrs exclude (#120804)
* Small cleanup to handling MATCH_ALL recorder attrs exclude

* Fix unneeded dict values for MATCH_ALL recorder attrs exclude

The exclude is a set so the dict values were not needed

* Fix unneeded dict values for MATCH_ALL recorder attrs exclude

The exclude is a set so the dict values were not needed

* Fix unneeded dict values for MATCH_ALL recorder attrs exclude

The exclude is a set so the dict values were not needed
2024-06-29 17:51:18 +02:00
J. Nick Koston 0ec07001bd Fix blocking I/O in xmpp notify to read uploaded files (#120801)
detected by ruff in https://github.com/home-assistant/core/pull/120799
2024-06-29 17:51:15 +02:00
J. Nick Koston 0dcfd38cdc Fix missing f-string in loop util (#120800) 2024-06-29 17:51:12 +02:00
Klaas Schoute b45eff9a2b Bump gridnet lib to v5.0.1 (#120793) 2024-06-29 17:51:09 +02:00
Klaas Schoute ec577c7bd3 Bump odp-amsterdam lib to v6.0.2 (#120788) 2024-06-29 17:51:06 +02:00
Paul Bottein 723c4a1eb5 Update frontend to 20240628.0 (#120785)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-06-29 17:51:02 +02:00
Klaas Schoute b30b4d5a3a Bump energyzero lib to v2.1.1 (#120783) 2024-06-29 17:50:59 +02:00
Matthew FitzGerald-Chamberlain 8165acddeb Bump pyaprilaire to 0.7.4 (#120782) 2024-06-29 17:50:56 +02:00
Joost Lekkerkerker 0f3ed3bb67 Bump aiowithings to 3.0.2 (#120765) 2024-06-29 17:50:53 +02:00
Maciej Bieniek d1a96ef362 Do not call async_delete_issue() if there is no issue to delete in Shelly integration (#120762)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-06-29 17:50:50 +02:00
J. Nick Koston 917eeba984 Increase mqtt availablity timeout to 50s (#120760) 2024-06-29 17:50:46 +02:00
Clifford Roche 59bb8b360e Bump greeclimate to 1.4.6 (#120758) 2024-06-29 17:50:43 +02:00
Klaas Schoute 6028e5b77a Bump p1monitor lib to v3.0.1 (#120756) 2024-06-29 17:50:40 +02:00
Klaas Schoute 83df470307 Bump easyenergy lib to v2.1.2 (#120753) 2024-06-29 17:50:37 +02:00
Joost Lekkerkerker 20ac0aa7b1 Bump govee-local-api to 1.5.1 (#120747) 2024-06-29 17:50:34 +02:00
Joost Lekkerkerker f57c942901 Bump sense-energy to 0.12.4 (#120744)
* Bump sense-energy to 0.12.4

* Fix
2024-06-29 17:50:31 +02:00
Alexey ALERT Rubashёff 8994ab1686 Add warm water remaining volume sensor to Overkiz (#120718)
* warm water remaining volume sensor

* Update homeassistant/components/overkiz/sensor.py

Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com>

---------

Co-authored-by: Dave T <17680170+davet2001@users.noreply.github.com>
2024-06-29 17:50:28 +02:00
Alexey ALERT Rubashёff b350ba9657 Add electrical consumption sensor to Overkiz (#120717)
electrical consumption sensor
2024-06-29 17:50:25 +02:00
wittypluck 5fd589053a Reject small uptime updates for Unifi clients (#120398)
Extend logic to reject small uptime updates to Unifi clients + add unit tests
2024-06-29 17:50:22 +02:00
Allen Porter 2d5961fa4f Bump gcal_sync to 6.1.3 (#120278) 2024-06-29 17:50:18 +02:00
Franck Nijhof d7a59748cf Bump version to 2024.7.0b4 2024-06-28 13:38:24 +02:00
tronikos cada78496b Fix Google Generative AI: 400 Request contains an invalid argument (#120741) 2024-06-28 13:31:00 +02:00
Illia c5fa9ad272 Bump asyncarve to 0.1.1 (#120740) 2024-06-28 13:30:51 +02:00
epenet fe8b5656dd Separate renault strings (#120737) 2024-06-28 13:30:48 +02:00
epenet 0ae11b0335 Bump renault-api to 0.2.4 (#120727) 2024-06-28 13:30:45 +02:00
Dave Leaver 76780ca04e Bump airtouch5py to 1.2.0 (#120715)
* Bump airtouch5py to fix console 1.2.0

* Bump airtouch5py again
2024-06-28 13:30:38 +02:00
Brett Adams 3932ce57b9 Check Tessie scopes to fix startup bug (#120710)
* Add scope check

* Add tests

* Bump Teslemetry
2024-06-28 13:30:35 +02:00
dougiteixeira 35d145d3bc Link the Statistics helper entity to the source entity device (#120705) 2024-06-28 13:30:30 +02:00
Maciej Bieniek 1227d56aa2 Bump nextdns to version 3.1.0 (#120703)
Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com>
2024-06-28 13:30:27 +02:00
Joost Lekkerkerker ef3ecb6183 Bump apsystems-ez1 to 1.3.3 (#120702) 2024-06-28 13:30:18 +02:00
Joost Lekkerkerker ca515f740e Bump panasonic_viera to 0.4.2 (#120692)
* Bump panasonic_viera to 0.4.2

* Bump panasonic_viera to 0.4.2

* Bump panasonic_viera to 0.4.2

* Fix Keys
2024-06-28 13:30:15 +02:00
Erik Montnemery 876fb234ce Bump hatasmota to 0.9.2 (#120670) 2024-06-28 13:30:10 +02:00
Erik Montnemery f28cbf1909 Set stateclass on unknown numeric Tasmota sensors (#120650) 2024-06-28 13:30:06 +02:00
Franck Nijhof 9b5d0f72dc Bump version to 2024.7.0b3 2024-06-27 22:20:25 +02:00
Steven B 23056f839b Update tplink unlink identifiers to deal with ids from other domains (#120596) 2024-06-27 22:20:02 +02:00
Joost Lekkerkerker 0b8dd738f1 Bump ttls to 1.8.3 (#120700) 2024-06-27 22:19:25 +02:00
Glenn Waters 411633d3b3 Bump Environment Canada to 0.7.1 (#120699) 2024-06-27 22:19:22 +02:00
Thomas55555 f3ab3bd5cb Bump aioautomower to 2024.6.3 (#120697) 2024-06-27 22:19:19 +02:00
Bram Kragten 476b9909ac Update frontend to 20240627.0 (#120693) 2024-06-27 22:19:16 +02:00
Glenn Waters e756328d52 Bump upb-lib to 0.5.7 (#120689) 2024-06-27 22:19:13 +02:00
J. Nick Koston b9c9921847 Add newer models to unifi integrations discovery (#120688) 2024-06-27 22:19:10 +02:00
MatthewFlamm 09dbd8e7eb Use more observations in NWS (#120687)
Use more observations
2024-06-27 22:19:07 +02:00
Glenn Waters 07dd832c58 Bump Environment Canada to 0.7.0 (#120686) 2024-06-27 22:19:04 +02:00
J. Nick Koston f9c5661c66 Bump unifi-discovery to 1.2.0 (#120684) 2024-06-27 22:19:01 +02:00
J. Nick Koston 94f8f8281f Bump uiprotect to 4.2.0 (#120669) 2024-06-27 22:18:58 +02:00
Erik Montnemery f6aa25c717 Fix docstring for EventStateEventData (#120662) 2024-06-27 22:18:55 +02:00
Jesse Hills f9ca85735d [esphome] Add more tests to bring integration to 100% coverage (#120661) 2024-06-27 22:18:52 +02:00
Joost Lekkerkerker be086c581c Fix Airgradient ABC days name (#120659) 2024-06-27 22:18:49 +02:00
Joost Lekkerkerker 03d198dd64 Fix unknown attribute in MPD (#120657) 2024-06-27 22:18:47 +02:00
Joost Lekkerkerker a8d6866f9f Disable polling for Knocki (#120656) 2024-06-27 22:18:44 +02:00
Brett Adams 0e1dc9878f Fix values at startup for Tessie (#120652) 2024-06-27 22:18:41 +02:00
Erik Montnemery 6849597764 Bump hatasmota to 0.9.1 (#120649) 2024-06-27 22:18:38 +02:00
Josef Zweck 3022d3bfa0 Move Auto On/off switches to Config EntityCategory (#120648) 2024-06-27 22:18:35 +02:00
Erik Montnemery 4836d6620b Add snapshots to tasmota sensor test (#120647) 2024-06-27 22:18:32 +02:00
Erik Montnemery b290e95350 Improve typing of state event helpers (#120639) 2024-06-27 22:18:29 +02:00
Josef Zweck 89ac3ce832 Fix the version that raises the issue (#120638) 2024-06-27 22:18:26 +02:00
Erik Montnemery 1933454b76 Rename async_track_state_reported_event to async_track_state_report_event (#120637)
* Rename async_track_state_reported_event to async_track_state_report_event

* Update tests
2024-06-27 22:18:23 +02:00
J. Nick Koston 38601d48ff Add async_track_state_reported_event to fix integration performance regression (#120622)
split from https://github.com/home-assistant/core/pull/120621
2024-06-27 22:18:20 +02:00
J. Nick Koston 7256f23376 Fix performance regression in integration from state_reported (#120621)
* Fix performance regression in integration from state_reported

Because the callbacks were no longer indexed by entity id, users
saw upwards of 1M calls/min

https://github.com/home-assistant/core/pull/113869/files#r1655580523

* Update homeassistant/helpers/event.py

* coverage

---------

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2024-06-27 22:18:17 +02:00
J. Nick Koston 7519603bf5 Bump uiprotect to 4.0.0 (#120617) 2024-06-27 22:18:14 +02:00
Luke Lashley ef47daad9d Bump anova_wifi to 0.14.0 (#120616) 2024-06-27 22:18:11 +02:00
Erik Montnemery 18d283bed6 Don't allow updating a device to have no connections or identifiers (#120603)
* Don't allow updating a device to have no connections or identifiers

* Move check to the top of the function
2024-06-27 22:18:08 +02:00
Steven B 210e906a4d Store tplink credentials_hash outside of device_config (#120597) 2024-06-27 22:17:51 +02:00
J. Nick Koston dcffd6bd7a Remove unused fields from unifiprotect event sensors (#120568) 2024-06-27 22:14:15 +02:00
Alexey ALERT Rubashёff 2c2261254b Improve AtlanticDomesticHotWaterProductionMBLComponent support in Overkiz (#114178)
* add overkiz AtlanticDHW support

Adds support of Overkiz water heater entity selection based on device controllable_name
Adds support of Atlantic water heater based on Atlantic Steatite Cube WI-FI VM 150 S4CS 2400W
Adds more Overkiz water heater binary_sensors, numbers, and sensors

* Changed class annotation

* min_temp and max_temp as properties

* reverted binary_sensors, number, sensor to make separate PRs

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/water_heater.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* review fixes, typos, and pylint

* review fix

* review fix

* ruff

* temperature properties changed to constructor attributes

* logger removed

* constants usage consistency

* redundant mapping removed

* Update homeassistant/components/overkiz/water_heater_entities/atlantic_dhw.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* boost mode method annotation typo

* removed away mode for atlantic dwh

* absence and boost mode attributes now support 'prog' state

* heating status bugfix

* electrical consumption sensor

* warm water remaining volume sensor

* away mode reintroduced

* mypy check

* boost plus state support

* Update homeassistant/components/overkiz/sensor.py

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>

* sensors reverted to separate them into their own PR

* check away and boost modes on before switching them off

* atlantic_dhw renamed to atlantic_domestic_hot_water_production

* annotation changed

* AtlanticDomesticHotWaterProductionMBLComponent file renamed, annotation change reverted

---------

Co-authored-by: Mick Vleeshouwer <mick@imick.nl>
2024-06-27 22:14:13 +02:00
Jesse Hills 53e49861a1 Mark esphome integration as platinum (#112565)
Co-authored-by: J. Nick Koston <nick@koston.org>
2024-06-27 22:14:08 +02:00
Franck Nijhof 3da8d0a741 Bump version to 2024.7.0b2 2024-06-26 23:55:20 +02:00
Paul Bottein 0701b0daa9 Update frontend to 20240626.2 (#120614) 2024-06-26 23:55:11 +02:00
Luca Angemi bea6fe30b8 Fix telegram bot thread_id key error (#120613) 2024-06-26 23:55:08 +02:00
Franck Nijhof 7d5d81b229 Bump version to 2024.7.0b1 2024-06-26 22:51:27 +02:00
Franck Nijhof 242b3fa609 Update adguardhome to 0.7.0 (#120605) 2024-06-26 22:48:39 +02:00
Marc Mueller 74204e2ee6 Fix mqtt test fixture usage (#120602) 2024-06-26 22:47:52 +02:00
Jan Bouwhuis da01635a07 Prevent changes to mutable bmw_connected_drive fixture data (#120600) 2024-06-26 22:47:32 +02:00
Shay Levy b5c34808e6 Add last_error reporting to Shelly diagnostics (#120595) 2024-06-26 22:47:09 +02:00
Erik Montnemery 80e70993c8 Remove deprecated run_immediately flag from integration sensor (#120593) 2024-06-26 22:46:50 +02:00
Max 2e01e169ef Correct deprecation warning async_register_static_paths (#120592) 2024-06-26 22:46:30 +02:00
Markus Jacobsen b35442ed2d Improve Bang & Olufsen error messages (#120587)
* Convert logger messages to raised errors where applicable

* Modify exception types

* Improve deezer / tidal error message

* Update homeassistant/components/bang_olufsen/strings.json

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

* Update homeassistant/components/bang_olufsen/media_player.py

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2024-06-26 22:46:27 +02:00
Michael Hansen 1b45069620 Bump intents to 2024.6.26 (#120584)
Bump intents
2024-06-26 22:46:23 +02:00
Shay Levy d3d0e05817 Change Shelly connect task log message level to error (#120582) 2024-06-26 22:46:20 +02:00
TheJulianJES 3d164c6721 Bump ZHA dependencies (#120581) 2024-06-26 22:46:17 +02:00
Florian 6dd1e09354 Don't allow switch toggle when device in locked in AVM FRITZ!SmartHome (#120132)
* fix: set state of the FritzBox-Switch to disabled if the option for manuel switching in the userinterface is disabled

* feat: raise an error instead of disabling switch

* feat: rename method signature

* fix: tests

* fix: wrong import

* feat: Update homeassistant/components/fritzbox/strings.json

Update error message

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

* Update tests/components/fritzbox/test_switch.py

feat: update test

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>

* make ruff happy

* fix expected error message check

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2024-06-26 22:46:13 +02:00
Shay Levy ba456f2564 Align Shelly sleeping devices timeout with non-sleeping (#118969) 2024-06-26 22:46:06 +02:00
Franck Nijhof 3492171ff8 Bump version to 2024.7.0b0 2024-06-26 16:17:57 +02:00
160 changed files with 4665 additions and 826 deletions
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"loggers": ["adguardhome"],
"requirements": ["adguardhome==0.6.3"]
"requirements": ["adguardhome==0.7.0"]
}
@@ -8,6 +8,34 @@
"default": "mdi:lightbulb-on-outline"
}
},
"number": {
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
}
},
"select": {
"configuration_control": {
"default": "mdi:cloud-cog"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"sensor": {
"total_volatile_organic_component_index": {
"default": "mdi:molecule"
@@ -17,6 +45,32 @@
},
"pm003_count": {
"default": "mdi:blur"
},
"led_bar_brightness": {
"default": "mdi:brightness-percent"
},
"display_brightness": {
"default": "mdi:brightness-percent"
},
"display_temperature_unit": {
"default": "mdi:thermometer-lines"
},
"led_bar_mode": {
"default": "mdi:led-strip"
},
"nox_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"voc_index_learning_time_offset": {
"default": "mdi:clock-outline"
},
"co2_automatic_baseline_calibration": {
"default": "mdi:molecule-co2"
}
},
"switch": {
"post_data_to_airgradient": {
"default": "mdi:cogs"
}
}
}
@@ -88,6 +88,7 @@ LEARNING_TIME_OFFSET_OPTIONS = [
]
ABC_DAYS = [
"1",
"8",
"30",
"90",
@@ -91,8 +91,9 @@
}
},
"co2_automatic_baseline_calibration": {
"name": "CO2 automatic baseline calibration",
"name": "CO2 automatic baseline duration",
"state": {
"1": "1 day",
"8": "8 days",
"30": "30 days",
"90": "90 days",
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push",
"loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.2.8"]
"requirements": ["airtouch5py==0.2.10"]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/anova",
"iot_class": "cloud_push",
"loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.12.0"]
"requirements": ["anova-wifi==0.14.0"]
}
@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.7.0"]
"requirements": ["pyaprilaire==0.7.4"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apsystems",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["apsystems-ez1==1.3.1"]
"requirements": ["apsystems-ez1==1.3.3"]
}
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arve",
"iot_class": "cloud_polling",
"requirements": ["asyncarve==0.0.9"]
"requirements": ["asyncarve==0.1.1"]
}
@@ -45,7 +45,7 @@ from homeassistant.components.media_player import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -316,7 +316,7 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
@callback
def _async_update_playback_error(self, data: PlaybackError) -> None:
"""Show playback error."""
_LOGGER.error(data.error)
raise HomeAssistantError(data.error)
@callback
def _async_update_playback_progress(self, data: PlaybackProgress) -> None:
@@ -516,7 +516,9 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
self.async_write_ha_state()
else:
_LOGGER.error("Seeking is currently only supported when using Deezer")
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="non_deezer_seeking"
)
async def async_media_previous_track(self) -> None:
"""Send the previous track command."""
@@ -529,12 +531,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
async def async_select_source(self, source: str) -> None:
"""Select an input source."""
if source not in self._sources.values():
_LOGGER.error(
"Invalid source: %s. Valid sources are: %s",
source,
list(self._sources.values()),
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_source",
translation_placeholders={
"invalid_source": source,
"valid_sources": ",".join(list(self._sources.values())),
},
)
return
key = [x for x in self._sources if self._sources[x] == source][0]
@@ -559,12 +563,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
media_type = MediaType.MUSIC
if media_type not in VALID_MEDIA_TYPES:
_LOGGER.error(
"%s is an invalid type. Valid values are: %s",
media_type,
VALID_MEDIA_TYPES,
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_media_type",
translation_placeholders={
"invalid_media_type": media_type,
"valid_media_types": ",".join(VALID_MEDIA_TYPES),
},
)
return
if media_source.is_media_source_id(media_id):
sourced_media = await media_source.async_resolve_media(
@@ -681,7 +687,14 @@ class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
)
except ApiException as error:
_LOGGER.error(json.loads(error.body)["message"])
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="play_media_error",
translation_placeholders={
"media_type": media_type,
"error_message": json.loads(error.body)["message"],
},
) from error
async def async_browse_media(
self,
@@ -28,6 +28,18 @@
"exceptions": {
"m3u_invalid_format": {
"message": "Media sources with the .m3u extension are not supported."
},
"non_deezer_seeking": {
"message": "Seeking is currently only supported when using Deezer"
},
"invalid_source": {
"message": "Invalid source: {invalid_source}. Valid sources are: {valid_sources}"
},
"invalid_media_type": {
"message": "{invalid_media_type} is an invalid type. Valid values are: {valid_media_types}."
},
"play_media_error": {
"message": "An error occurred while attempting to play {media_type}: {error_message}."
}
}
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/conversation",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.21"]
"requirements": ["hassil==1.7.1", "home-assistant-intents==2024.6.26"]
}
+7 -7
View File
@@ -4,11 +4,11 @@ from datetime import timedelta
import logging
from pizzapi import Address, Customer, Order
from pizzapi.address import StoreException
import voluptuous as vol
from homeassistant.components import http
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_component import EntityComponent
@@ -118,7 +118,7 @@ class Dominos:
self.country = conf.get(ATTR_COUNTRY)
try:
self.closest_store = self.address.closest_store()
except StoreException:
except Exception: # noqa: BLE001
self.closest_store = None
def handle_order(self, call: ServiceCall) -> None:
@@ -139,7 +139,7 @@ class Dominos:
"""Update the shared closest store (if open)."""
try:
self.closest_store = self.address.closest_store()
except StoreException:
except Exception: # noqa: BLE001
self.closest_store = None
return False
return True
@@ -219,7 +219,7 @@ class DominosOrder(Entity):
"""Update the order state and refreshes the store."""
try:
self.dominos.update_closest_store()
except StoreException:
except Exception: # noqa: BLE001
self._orderable = False
return
@@ -227,13 +227,13 @@ class DominosOrder(Entity):
order = self.order()
order.pay_with()
self._orderable = True
except StoreException:
except Exception: # noqa: BLE001
self._orderable = False
def order(self):
"""Create the order object."""
if self.dominos.closest_store is None:
raise StoreException
raise HomeAssistantError("No store available")
order = Order(
self.dominos.closest_store,
@@ -252,7 +252,7 @@ class DominosOrder(Entity):
try:
order = self.order()
order.place()
except StoreException:
except Exception: # noqa: BLE001
self._orderable = False
_LOGGER.warning(
"Attempted to order Dominos - Order invalid or store closed"
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/dominos",
"iot_class": "cloud_polling",
"loggers": ["pizzapi"],
"requirements": ["pizzapi==0.0.3"]
"requirements": ["pizzapi==0.0.6"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/easyenergy",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["easyenergy==2.1.1"]
"requirements": ["easyenergy==2.1.2"]
}
@@ -6,5 +6,5 @@
"iot_class": "local_push",
"loggers": ["sense_energy"],
"quality_scale": "internal",
"requirements": ["sense-energy==0.12.2"]
"requirements": ["sense-energy==0.12.4"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/energyzero",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["energyzero==2.1.0"]
"requirements": ["energyzero==2.1.1"]
}
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/environment_canada",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.6.3"]
"requirements": ["env-canada==0.7.1"]
}
@@ -19,7 +19,6 @@ from homeassistant.const import (
PERCENTAGE,
UV_INDEX,
UnitOfLength,
UnitOfPrecipitationDepth,
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
@@ -114,14 +113,6 @@ SENSOR_TYPES: tuple[ECSensorEntityDescription, ...] = (
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda data: data.conditions.get("pop", {}).get("value"),
),
ECSensorEntityDescription(
key="precip_yesterday",
translation_key="precip_yesterday",
device_class=SensorDeviceClass.PRECIPITATION,
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data.conditions.get("precip_yesterday", {}).get("value"),
),
ECSensorEntityDescription(
key="pressure",
translation_key="pressure",
@@ -52,9 +52,6 @@
"pop": {
"name": "Chance of precipitation"
},
"precip_yesterday": {
"name": "Precipitation yesterday"
},
"pressure": {
"name": "Barometric pressure"
},
@@ -15,6 +15,7 @@
"iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==24.6.1",
"esphome-dashboard-api==1.2.3",
@@ -98,7 +98,7 @@ class FlumeNotificationDataUpdateCoordinator(DataUpdateCoordinator[None]):
# The related binary sensors (leak detected, high flow, low battery)
# will be active until the notification is deleted in the Flume app.
self.notifications = pyflume.FlumeNotificationList(
self.auth, read=None, sort_direction="DESC"
self.auth, read=None
).notification_list
_LOGGER.debug("Notifications %s", self.notifications)
+1 -1
View File
@@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/flume",
"iot_class": "cloud_polling",
"loggers": ["pyflume"],
"requirements": ["PyFlume==0.8.7"]
"requirements": ["PyFlume==0.6.5"]
}
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["pyfritzhome"],
"quality_scale": "gold",
"requirements": ["pyfritzhome==0.6.11"],
"requirements": ["pyfritzhome==0.6.12"],
"ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"
@@ -81,6 +81,9 @@
}
},
"exceptions": {
"manual_switching_disabled": {
"message": "Can't toggle switch while manual switching is disabled for the device."
},
"change_preset_while_active_mode": {
"message": "Can't change preset while holiday or summer mode is active on the device."
},
@@ -6,9 +6,11 @@ from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import FritzBoxDeviceEntity
from .const import DOMAIN
from .coordinator import FritzboxConfigEntry
@@ -48,10 +50,20 @@ class FritzboxSwitch(FritzBoxDeviceEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self.check_lock_state()
await self.hass.async_add_executor_job(self.data.set_switch_state_on)
await self.coordinator.async_refresh()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self.check_lock_state()
await self.hass.async_add_executor_job(self.data.set_switch_state_off)
await self.coordinator.async_refresh()
def check_lock_state(self) -> None:
"""Raise an Error if manual switching via FRITZ!Box user interface is disabled."""
if self.data.lock:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="manual_switching_disabled",
)
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20240626.0"]
"requirements": ["home-assistant-frontend==20240628.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/garages_amsterdam",
"iot_class": "cloud_polling",
"requirements": ["odp-amsterdam==6.0.1"]
"requirements": ["odp-amsterdam==6.0.2"]
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/calendar.google",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.1"]
"requirements": ["gcal-sync==6.1.3", "oauth2client==4.1.3", "ical==8.0.1"]
}
@@ -95,9 +95,12 @@ def _format_tool(
) -> dict[str, Any]:
"""Format tool specification."""
parameters = _format_schema(
convert(tool.parameters, custom_serializer=custom_serializer)
)
if tool.parameters.schema:
parameters = _format_schema(
convert(tool.parameters, custom_serializer=custom_serializer)
)
else:
parameters = None
return protos.Tool(
{
@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==1.5.0"]
"requirements": ["govee-local-api==1.5.1"]
}
+1 -1
View File
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/gree",
"iot_class": "local_polling",
"loggers": ["greeclimate"],
"requirements": ["greeclimate==1.4.1"]
"requirements": ["greeclimate==1.4.6"]
}
+6 -7
View File
@@ -71,13 +71,12 @@ class HoneywellSwitch(SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on if heat mode is enabled."""
if self._device.system_mode == "heat":
try:
await self._device.set_system_mode("emheat")
except SomeComfortError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="switch_failed_on"
) from err
try:
await self._device.set_system_mode("emheat")
except SomeComfortError as err:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="switch_failed_on"
) from err
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off if on."""
+1 -1
View File
@@ -483,7 +483,7 @@ class HomeAssistantHTTP:
frame.report(
"calls hass.http.register_static_path which is deprecated because "
"it does blocking I/O in the event loop, instead "
"call `await hass.http.async_register_static_path("
"call `await hass.http.async_register_static_paths("
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`; '
"This function will be removed in 2025.7",
exclude_integrations={"http"},
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/husqvarna_automower",
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"requirements": ["aioautomower==2024.6.1"]
"requirements": ["aioautomower==2024.6.3"]
}
+11 -15
View File
@@ -27,8 +27,6 @@ from homeassistant.const import (
CONF_METHOD,
CONF_NAME,
CONF_UNIQUE_ID,
EVENT_STATE_CHANGED,
EVENT_STATE_REPORTED,
STATE_UNAVAILABLE,
UnitOfTime,
)
@@ -45,7 +43,11 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.event import (
async_call_later,
async_track_state_change_event,
async_track_state_report_event,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from .const import (
@@ -440,23 +442,17 @@ class IntegrationSensor(RestoreSensor):
self._derive_and_set_attributes_from_state(state)
self.async_on_remove(
self.hass.bus.async_listen(
EVENT_STATE_CHANGED,
async_track_state_change_event(
self.hass,
self._sensor_source_id,
handle_state_change,
event_filter=callback(
lambda event_data: event_data["entity_id"] == self._sensor_source_id
),
run_immediately=True,
)
)
self.async_on_remove(
self.hass.bus.async_listen(
EVENT_STATE_REPORTED,
async_track_state_report_event(
self.hass,
self._sensor_source_id,
handle_state_report,
event_filter=callback(
lambda event_data: event_data["entity_id"] == self._sensor_source_id
),
run_immediately=True,
)
)
@@ -28,7 +28,7 @@ class JewishCalendarEntity(Entity):
) -> None:
"""Initialize a Jewish Calendar entity."""
self.entity_description = description
self._attr_unique_id = f"{config_entry.entry_id}_{description.key}"
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
+1
View File
@@ -48,6 +48,7 @@ class KnockiTrigger(EventEntity):
_attr_event_types = [EVENT_TRIGGERED]
_attr_has_entity_name = True
_attr_should_poll = False
_attr_translation_key = "knocki"
def __init__(self, trigger: Trigger, client: KnockiClient) -> None:
@@ -112,7 +112,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
entry.runtime_data = coordinator
gateway_version = coordinator.device.firmware[FirmwareType.GATEWAY].current_version
if version.parse(gateway_version) < version.parse("v3.5-rc5"):
if version.parse(gateway_version) < version.parse("v3.4-rc5"):
# incompatible gateway firmware, create an issue
ir.async_create_issue(
hass,
@@ -9,6 +9,7 @@ from lmcloud.lm_machine import LaMarzoccoMachine
from lmcloud.models import LaMarzoccoMachineConfig
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -105,6 +106,7 @@ class LaMarzoccoAutoOnOffSwitchEntity(LaMarzoccoBaseEntity, SwitchEntity):
super().__init__(coordinator, f"auto_on_off_{identifier}")
self._identifier = identifier
self._attr_translation_placeholders = {"id": identifier}
self.entity_category = EntityCategory.CONFIG
async def _async_enable(self, state: bool) -> None:
"""Enable or disable the auto on/off schedule."""
+3 -6
View File
@@ -30,8 +30,8 @@ async def async_setup_entry(
def _get_event_from_mealplan(mealplan: Mealplan) -> CalendarEvent:
"""Create a CalendarEvent from a Mealplan."""
description: str | None = None
name = "No recipe"
description: str | None = mealplan.description
name = mealplan.title or "No recipe"
if mealplan.recipe:
name = mealplan.recipe.name
description = mealplan.recipe.description
@@ -50,12 +50,9 @@ class MealieMealplanCalendarEntity(MealieEntity, CalendarEntity):
self, coordinator: MealieCoordinator, entry_type: MealplanEntryType
) -> None:
"""Create the Calendar entity."""
super().__init__(coordinator)
super().__init__(coordinator, entry_type.name.lower())
self._entry_type = entry_type
self._attr_translation_key = entry_type.name.lower()
self._attr_unique_id = (
f"{self.coordinator.config_entry.entry_id}_{entry_type.name.lower()}"
)
@property
def event(self) -> CalendarEvent | None:
@@ -28,14 +28,13 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by the user."""
errors: dict[str, str] = {}
if user_input:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
client = MealieClient(
user_input[CONF_HOST],
token=user_input[CONF_API_TOKEN],
session=async_get_clientsession(self.hass),
)
try:
await client.get_mealplan_today()
info = await client.get_user_info()
except MealieConnectionError:
errors["base"] = "cannot_connect"
except MealieAuthenticationError:
@@ -44,6 +43,8 @@ class MealieConfigFlow(ConfigFlow, domain=DOMAIN):
LOGGER.exception("Unexpected error")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info.user_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title="Mealie",
data=user_input,
+5 -2
View File
@@ -12,10 +12,13 @@ class MealieEntity(CoordinatorEntity[MealieCoordinator]):
_attr_has_entity_name = True
def __init__(self, coordinator: MealieCoordinator) -> None:
def __init__(self, coordinator: MealieCoordinator, key: str) -> None:
"""Initialize Mealie entity."""
super().__init__(coordinator)
unique_id = coordinator.config_entry.unique_id
assert unique_id is not None
self._attr_unique_id = f"{unique_id}_{key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
identifiers={(DOMAIN, unique_id)},
entry_type=DeviceEntryType.SERVICE,
)
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/mealie",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["aiomealie==0.4.0"]
"requirements": ["aiomealie==0.5.0"]
}
@@ -421,11 +421,6 @@ class MpdDevice(MediaPlayerEntity):
"""Name of the current input source."""
return self._current_playlist
@property
def source_list(self):
"""Return the list of available input sources."""
return self._playlists
async def async_select_source(self, source: str) -> None:
"""Choose a different available playlist and play it."""
await self.async_play_media(MediaType.PLAYLIST, source)
+1 -1
View File
@@ -36,7 +36,7 @@ from .const import (
)
from .models import DATA_MQTT, DATA_MQTT_AVAILABLE, ReceiveMessage
AVAILABILITY_TIMEOUT = 30.0
AVAILABILITY_TIMEOUT = 50.0
TEMP_DIR_NAME = f"home-assistant-{DOMAIN}"
+3 -3
View File
@@ -18,6 +18,7 @@ from nextdns import (
NextDns,
Settings,
)
from tenacity import RetryError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
@@ -84,9 +85,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b
websession = async_get_clientsession(hass)
try:
async with asyncio.timeout(10):
nextdns = await NextDns.create(websession, api_key)
except (ApiError, ClientConnectorError, TimeoutError) as err:
nextdns = await NextDns.create(websession, api_key)
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
raise ConfigEntryNotReady from err
tasks = []
@@ -2,11 +2,11 @@
from __future__ import annotations
import asyncio
from typing import Any
from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, InvalidApiKeyError, NextDns
from tenacity import RetryError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -37,13 +37,12 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input is not None:
self.api_key = user_input[CONF_API_KEY]
try:
async with asyncio.timeout(10):
self.nextdns = await NextDns.create(
websession, user_input[CONF_API_KEY]
)
self.nextdns = await NextDns.create(
websession, user_input[CONF_API_KEY]
)
except InvalidApiKeyError:
errors["base"] = "invalid_api_key"
except (ApiError, ClientConnectorError, TimeoutError):
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
errors["base"] = "unknown"
@@ -1,6 +1,5 @@
"""NextDns coordinator."""
import asyncio
from datetime import timedelta
import logging
from typing import TypeVar
@@ -19,6 +18,7 @@ from nextdns import (
Settings,
)
from nextdns.model import NextDnsData
from tenacity import RetryError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
@@ -58,9 +58,13 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
async def _async_update_data(self) -> CoordinatorDataT:
"""Update data via internal method."""
try:
async with asyncio.timeout(10):
return await self._async_update_data_internal()
except (ApiError, ClientConnectorError, InvalidApiKeyError) as err:
return await self._async_update_data_internal()
except (
ApiError,
ClientConnectorError,
InvalidApiKeyError,
RetryError,
) as err:
raise UpdateFailed(err) from err
async def _async_update_data_internal(self) -> CoordinatorDataT:
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["nextdns"],
"quality_scale": "platinum",
"requirements": ["nextdns==3.0.0"]
"requirements": ["nextdns==3.1.0"]
}
+1 -1
View File
@@ -261,7 +261,7 @@ class NMBSSensor(SensorEntity):
attrs["via_arrival_platform"] = via["arrival"]["platform"]
attrs["via_transfer_platform"] = via["departure"]["platform"]
attrs["via_transfer_time"] = get_delay_in_minutes(
via["timeBetween"]
via["timebetween"]
) + get_delay_in_minutes(via["departure"]["delay"])
if delay > 0:
+2 -2
View File
@@ -78,8 +78,8 @@ HOURLY = "hourly"
OBSERVATION_VALID_TIME = timedelta(minutes=60)
FORECAST_VALID_TIME = timedelta(minutes=45)
# A lot of stations update once hourly plus some wiggle room
UPDATE_TIME_PERIOD = timedelta(minutes=70)
# Ask for observations for last four hours
UPDATE_TIME_PERIOD = timedelta(minutes=240)
DEBOUNCE_TIME = 10 * 60 # in seconds
DEFAULT_SCAN_INTERVAL = timedelta(minutes=10)
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/opensky",
"iot_class": "cloud_polling",
"requirements": ["python-opensky==1.0.0"]
"requirements": ["python-opensky==1.0.1"]
}
@@ -109,17 +109,20 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [
key=OverkizState.CORE_HEATING_STATUS,
name="Heating status",
device_class=BinarySensorDeviceClass.HEAT,
value_fn=lambda state: state == OverkizCommandParam.ON,
value_fn=lambda state: cast(str, state).lower()
in (OverkizCommandParam.ON, OverkizCommandParam.HEATING),
),
OverkizBinarySensorDescription(
key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE,
name="Absence mode",
value_fn=lambda state: state == OverkizCommandParam.ON,
value_fn=lambda state: state
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
),
OverkizBinarySensorDescription(
key=OverkizState.MODBUSLINK_DHW_BOOST_MODE,
name="Boost mode",
value_fn=lambda state: state == OverkizCommandParam.ON,
value_fn=lambda state: state
in (OverkizCommandParam.ON, OverkizCommandParam.PROG),
),
]
@@ -182,6 +182,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
OverkizSensorDescription(
key=OverkizState.MODBUSLINK_POWER_HEAT_ELECTRICAL,
name="Electric power consumption",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
),
OverkizSensorDescription(
key=OverkizState.CORE_CONSUMPTION_TARIFF1,
name="Consumption tariff 1",
@@ -413,6 +420,13 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
OverkizSensorDescription(
key=OverkizState.CORE_REMAINING_HOT_WATER,
name="Warm water remaining",
device_class=SensorDeviceClass.VOLUME,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfVolume.LITERS,
),
# Cover
OverkizSensorDescription(
key=OverkizState.CORE_TARGET_CLOSURE,
@@ -9,7 +9,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantOverkizData
from .const import DOMAIN
from .water_heater_entities import WIDGET_TO_WATER_HEATER_ENTITY
from .entity import OverkizEntity
from .water_heater_entities import (
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY,
WIDGET_TO_WATER_HEATER_ENTITY,
)
async def async_setup_entry(
@@ -19,11 +23,20 @@ async def async_setup_entry(
) -> None:
"""Set up the Overkiz DHW from a config entry."""
data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id]
entities: list[OverkizEntity] = []
async_add_entities(
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
device.device_url, data.coordinator
)
for device in data.platforms[Platform.WATER_HEATER]
if device.widget in WIDGET_TO_WATER_HEATER_ENTITY
)
for device in data.platforms[Platform.WATER_HEATER]:
if device.controllable_name in CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY:
entities.append(
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY[device.controllable_name](
device.device_url, data.coordinator
)
)
elif device.widget in WIDGET_TO_WATER_HEATER_ENTITY:
entities.append(
WIDGET_TO_WATER_HEATER_ENTITY[device.widget](
device.device_url, data.coordinator
)
)
async_add_entities(entities)
@@ -2,6 +2,9 @@
from pyoverkiz.enums.ui import UIWidget
from .atlantic_domestic_hot_water_production_mlb_component import (
AtlanticDomesticHotWaterProductionMBLComponent,
)
from .atlantic_pass_apc_dhw import AtlanticPassAPCDHW
from .domestic_hot_water_production import DomesticHotWaterProduction
from .hitachi_dhw import HitachiDHW
@@ -11,3 +14,7 @@ WIDGET_TO_WATER_HEATER_ENTITY = {
UIWidget.DOMESTIC_HOT_WATER_PRODUCTION: DomesticHotWaterProduction,
UIWidget.HITACHI_DHW: HitachiDHW,
}
CONTROLLABLE_NAME_TO_WATER_HEATER_ENTITY = {
"modbuslink:AtlanticDomesticHotWaterProductionMBLComponent": AtlanticDomesticHotWaterProductionMBLComponent,
}
@@ -0,0 +1,182 @@
"""Support for AtlanticDomesticHotWaterProductionMBLComponent."""
from typing import Any, cast
from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState
from homeassistant.components.water_heater import (
STATE_ECO,
STATE_OFF,
STATE_PERFORMANCE,
WaterHeaterEntity,
WaterHeaterEntityFeature,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from .. import OverkizDataUpdateCoordinator
from ..entity import OverkizEntity
class AtlanticDomesticHotWaterProductionMBLComponent(OverkizEntity, WaterHeaterEntity):
"""Representation of AtlanticDomesticHotWaterProductionMBLComponent (modbuslink)."""
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
WaterHeaterEntityFeature.TARGET_TEMPERATURE
| WaterHeaterEntityFeature.OPERATION_MODE
| WaterHeaterEntityFeature.AWAY_MODE
| WaterHeaterEntityFeature.ON_OFF
)
_attr_operation_list = [
OverkizCommandParam.PERFORMANCE,
OverkizCommandParam.ECO,
OverkizCommandParam.MANUAL,
]
def __init__(
self, device_url: str, coordinator: OverkizDataUpdateCoordinator
) -> None:
"""Init method."""
super().__init__(device_url, coordinator)
self._attr_max_temp = cast(
float,
self.executor.select_state(
OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE
),
)
self._attr_min_temp = cast(
float,
self.executor.select_state(
OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE
),
)
@property
def current_temperature(self) -> float:
"""Return the current temperature."""
return cast(
float,
self.executor.select_state(
OverkizState.MODBUSLINK_MIDDLE_WATER_TEMPERATURE
),
)
@property
def target_temperature(self) -> float:
"""Return the temperature corresponding to the PRESET."""
return cast(
float,
self.executor.select_state(OverkizState.CORE_WATER_TARGET_TEMPERATURE),
)
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new temperature."""
temperature = kwargs[ATTR_TEMPERATURE]
await self.executor.async_execute_command(
OverkizCommand.SET_TARGET_DHW_TEMPERATURE, temperature
)
@property
def is_boost_mode_on(self) -> bool:
"""Return true if boost mode is on."""
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_BOOST_MODE) in (
OverkizCommandParam.ON,
OverkizCommandParam.PROG,
)
@property
def is_eco_mode_on(self) -> bool:
"""Return true if eco mode is on."""
return self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE) in (
OverkizCommandParam.MANUAL_ECO_ACTIVE,
OverkizCommandParam.AUTO_MODE,
)
@property
def is_away_mode_on(self) -> bool:
"""Return true if away mode is on."""
return (
self.executor.select_state(OverkizState.MODBUSLINK_DHW_ABSENCE_MODE)
== OverkizCommandParam.ON
)
@property
def current_operation(self) -> str:
"""Return current operation."""
if self.is_away_mode_on:
return STATE_OFF
if self.is_boost_mode_on:
return STATE_PERFORMANCE
if self.is_eco_mode_on:
return STATE_ECO
if (
cast(str, self.executor.select_state(OverkizState.MODBUSLINK_DHW_MODE))
== OverkizCommandParam.MANUAL_ECO_INACTIVE
):
return OverkizCommandParam.MANUAL
return STATE_OFF
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new operation mode."""
if operation_mode in (STATE_PERFORMANCE, OverkizCommandParam.BOOST):
if self.is_away_mode_on:
await self.async_turn_away_mode_off()
await self.async_turn_boost_mode_on()
elif operation_mode in (
OverkizCommandParam.ECO,
OverkizCommandParam.MANUAL_ECO_ACTIVE,
):
if self.is_away_mode_on:
await self.async_turn_away_mode_off()
if self.is_boost_mode_on:
await self.async_turn_boost_mode_off()
await self.executor.async_execute_command(
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.AUTO_MODE
)
elif operation_mode in (
OverkizCommandParam.MANUAL,
OverkizCommandParam.MANUAL_ECO_INACTIVE,
):
if self.is_away_mode_on:
await self.async_turn_away_mode_off()
if self.is_boost_mode_on:
await self.async_turn_boost_mode_off()
await self.executor.async_execute_command(
OverkizCommand.SET_DHW_MODE, OverkizCommandParam.MANUAL_ECO_INACTIVE
)
else:
if self.is_away_mode_on:
await self.async_turn_away_mode_off()
if self.is_boost_mode_on:
await self.async_turn_boost_mode_off()
await self.executor.async_execute_command(
OverkizCommand.SET_DHW_MODE, operation_mode
)
async def async_turn_away_mode_on(self) -> None:
"""Turn away mode on."""
await self.executor.async_execute_command(
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.ON
)
async def async_turn_away_mode_off(self) -> None:
"""Turn away mode off."""
await self.executor.async_execute_command(
OverkizCommand.SET_ABSENCE_MODE, OverkizCommandParam.OFF
)
async def async_turn_boost_mode_on(self) -> None:
"""Turn boost mode on."""
await self.executor.async_execute_command(
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.ON
)
async def async_turn_boost_mode_off(self) -> None:
"""Turn boost mode off."""
await self.executor.async_execute_command(
OverkizCommand.SET_BOOST_MODE, OverkizCommandParam.OFF
)
@@ -7,5 +7,5 @@
"iot_class": "local_polling",
"loggers": ["p1monitor"],
"quality_scale": "platinum",
"requirements": ["p1monitor==3.0.0"]
"requirements": ["p1monitor==3.0.1"]
}
@@ -196,10 +196,10 @@ class Remote:
self.muted = self._control.get_mute()
self.volume = self._control.get_volume() / 100
async def async_send_key(self, key):
async def async_send_key(self, key: Keys | str) -> None:
"""Send a key to the TV and handle exceptions."""
try:
key = getattr(Keys, key)
key = getattr(Keys, key.upper())
except (AttributeError, TypeError):
key = getattr(key, "value", key)
@@ -211,13 +211,13 @@ class Remote:
await self._on_action.async_run(context=context)
await self.async_update()
elif self.state != STATE_ON:
await self.async_send_key(Keys.power)
await self.async_send_key(Keys.POWER)
await self.async_update()
async def async_turn_off(self):
"""Turn off the TV."""
if self.state != STATE_OFF:
await self.async_send_key(Keys.power)
await self.async_send_key(Keys.POWER)
self.state = STATE_OFF
await self.async_update()
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/panasonic_viera",
"iot_class": "local_polling",
"loggers": ["panasonic_viera"],
"requirements": ["panasonic-viera==0.3.6"]
"requirements": ["panasonic-viera==0.4.2"]
}
@@ -126,11 +126,11 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
async def async_volume_up(self) -> None:
"""Volume up the media player."""
await self._remote.async_send_key(Keys.volume_up)
await self._remote.async_send_key(Keys.VOLUME_UP)
async def async_volume_down(self) -> None:
"""Volume down media player."""
await self._remote.async_send_key(Keys.volume_down)
await self._remote.async_send_key(Keys.VOLUME_DOWN)
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
@@ -143,33 +143,33 @@ class PanasonicVieraTVEntity(MediaPlayerEntity):
async def async_media_play_pause(self) -> None:
"""Simulate play pause media player."""
if self._remote.playing:
await self._remote.async_send_key(Keys.pause)
await self._remote.async_send_key(Keys.PAUSE)
self._remote.playing = False
else:
await self._remote.async_send_key(Keys.play)
await self._remote.async_send_key(Keys.PLAY)
self._remote.playing = True
async def async_media_play(self) -> None:
"""Send play command."""
await self._remote.async_send_key(Keys.play)
await self._remote.async_send_key(Keys.PLAY)
self._remote.playing = True
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._remote.async_send_key(Keys.pause)
await self._remote.async_send_key(Keys.PAUSE)
self._remote.playing = False
async def async_media_stop(self) -> None:
"""Stop playback."""
await self._remote.async_send_key(Keys.stop)
await self._remote.async_send_key(Keys.STOP)
async def async_media_next_track(self) -> None:
"""Send the fast forward command."""
await self._remote.async_send_key(Keys.fast_forward)
await self._remote.async_send_key(Keys.FAST_FORWARD)
async def async_media_previous_track(self) -> None:
"""Send the rewind command."""
await self._remote.async_send_key(Keys.rewind)
await self._remote.async_send_key(Keys.REWIND)
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/pure_energie",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["gridnet==5.0.0"],
"requirements": ["gridnet==5.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",
+9 -13
View File
@@ -142,6 +142,13 @@ _DEFAULT_TABLE_ARGS = {
"mariadb_engine": MYSQL_ENGINE,
}
_MATCH_ALL_KEEP = {
ATTR_DEVICE_CLASS,
ATTR_STATE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
ATTR_FRIENDLY_NAME,
}
class UnusedDateTime(DateTime):
"""An unused column type that behaves like a datetime."""
@@ -597,19 +604,8 @@ class StateAttributes(Base):
if MATCH_ALL in unrecorded_attributes:
# Don't exclude device class, state class, unit of measurement
# or friendly name when using the MATCH_ALL exclude constant
_exclude_attributes = {
k: v
for k, v in state.attributes.items()
if k
not in (
ATTR_DEVICE_CLASS,
ATTR_STATE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
ATTR_FRIENDLY_NAME,
)
}
exclude_attrs.update(_exclude_attributes)
exclude_attrs.update(state.attributes)
exclude_attrs -= _MATCH_ALL_KEEP
else:
exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS
encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes
@@ -81,7 +81,7 @@ BINARY_SENSOR_TYPES: tuple[RenaultBinarySensorEntityDescription, ...] = tuple(
key="hvac_status",
coordinator="hvac_status",
on_key="hvacStatus",
on_value=2,
on_value="on",
translation_key="hvac_status",
),
RenaultBinarySensorEntityDescription(
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["renault_api"],
"quality_scale": "platinum",
"requirements": ["renault-api==0.2.3"]
"requirements": ["renault-api==0.2.4"]
}
@@ -73,9 +73,9 @@
"charge_mode": {
"name": "Charge mode",
"state": {
"always": "Instant",
"always_charging": "[%key:component::renault::entity::select::charge_mode::state::always%]",
"schedule_mode": "Planner",
"always": "Always",
"always_charging": "Always charging",
"schedule_mode": "Schedule mode",
"scheduled": "Scheduled"
}
}
+1 -1
View File
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/sense",
"iot_class": "cloud_polling",
"loggers": ["sense_energy"],
"requirements": ["sense-energy==0.12.2"]
"requirements": ["sense-energy==0.12.4"]
}
+1 -3
View File
@@ -83,11 +83,9 @@ REST_SENSORS_UPDATE_INTERVAL: Final = 60
# Refresh interval for RPC polling sensors
RPC_SENSORS_POLLING_INTERVAL: Final = 60
# Multiplier used to calculate the "update_interval" for sleeping devices.
SLEEP_PERIOD_MULTIPLIER: Final = 1.2
CONF_SLEEP_PERIOD: Final = "sleep_period"
# Multiplier used to calculate the "update_interval" for non-sleeping devices.
# Multiplier used to calculate the "update_interval" for shelly devices.
UPDATE_PERIOD_MULTIPLIER: Final = 2.2
# Reconnect interval for GEN2 devices
+11 -11
View File
@@ -54,7 +54,6 @@ from .const import (
RPC_RECONNECT_INTERVAL,
RPC_SENSORS_POLLING_INTERVAL,
SHBTN_MODELS,
SLEEP_PERIOD_MULTIPLIER,
UPDATE_PERIOD_MULTIPLIER,
BLEScannerMode,
)
@@ -168,7 +167,7 @@ class ShellyCoordinatorBase[_DeviceT: BlockDevice | RpcDevice](
await self.device.initialize()
update_device_fw_info(self.hass, self.device, self.entry)
except DeviceConnectionError as err:
LOGGER.debug(
LOGGER.error(
"Error connecting to Shelly device %s, error: %r", self.name, err
)
return False
@@ -229,7 +228,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
"""Initialize the Shelly block device coordinator."""
self.entry = entry
if self.sleep_period:
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
else:
update_interval = (
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
@@ -378,12 +377,13 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]):
eager_start=True,
)
elif update_type is BlockUpdateType.COAP_PERIODIC:
if self._push_update_failures >= MAX_PUSH_UPDATE_FAILURES:
ir.async_delete_issue(
self.hass,
DOMAIN,
PUSH_UPDATE_ISSUE_ID.format(unique=self.mac),
)
self._push_update_failures = 0
ir.async_delete_issue(
self.hass,
DOMAIN,
PUSH_UPDATE_ISSUE_ID.format(unique=self.mac),
)
elif update_type is BlockUpdateType.COAP_REPLY:
self._push_update_failures += 1
if self._push_update_failures == MAX_PUSH_UPDATE_FAILURES:
@@ -429,7 +429,7 @@ class ShellyRestCoordinator(ShellyCoordinatorBase[BlockDevice]):
in BATTERY_DEVICES_WITH_PERMANENT_CONNECTION
):
update_interval = (
SLEEP_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
UPDATE_PERIOD_MULTIPLIER * device.settings["coiot"]["update_period"]
)
super().__init__(hass, entry, device, update_interval)
@@ -459,7 +459,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
"""Initialize the Shelly RPC device coordinator."""
self.entry = entry
if self.sleep_period:
update_interval = SLEEP_PERIOD_MULTIPLIER * self.sleep_period
update_interval = UPDATE_PERIOD_MULTIPLIER * self.sleep_period
else:
update_interval = RPC_RECONNECT_INTERVAL
super().__init__(hass, entry, device, update_interval)
@@ -486,7 +486,7 @@ class ShellyRpcCoordinator(ShellyCoordinatorBase[RpcDevice]):
data[CONF_SLEEP_PERIOD] = wakeup_period
self.hass.config_entries.async_update_entry(self.entry, data=data)
update_interval = SLEEP_PERIOD_MULTIPLIER * wakeup_period
update_interval = UPDATE_PERIOD_MULTIPLIER * wakeup_period
self.update_interval = timedelta(seconds=update_interval)
return True
@@ -24,6 +24,8 @@ async def async_get_config_entry_diagnostics(
device_settings: str | dict = "not initialized"
device_status: str | dict = "not initialized"
bluetooth: str | dict = "not initialized"
last_error: str = "not initialized"
if shelly_entry_data.block:
block_coordinator = shelly_entry_data.block
assert block_coordinator
@@ -55,6 +57,10 @@ async def async_get_config_entry_diagnostics(
"uptime",
]
}
if block_coordinator.device.last_error:
last_error = repr(block_coordinator.device.last_error)
else:
rpc_coordinator = shelly_entry_data.rpc
assert rpc_coordinator
@@ -79,6 +85,9 @@ async def async_get_config_entry_diagnostics(
"scanner": await scanner.async_diagnostics(),
}
if rpc_coordinator.device.last_error:
last_error = repr(rpc_coordinator.device.last_error)
if isinstance(device_status, dict):
device_status = async_redact_data(device_status, ["ssid"])
@@ -87,5 +96,6 @@ async def async_get_config_entry_diagnostics(
"device_info": device_info,
"device_settings": device_settings,
"device_status": device_status,
"last_error": last_error,
"bluetooth": bluetooth,
}
@@ -1,8 +1,11 @@
"""The statistics component."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.const import CONF_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device import (
async_remove_stale_devices_links_keep_entity_device,
)
DOMAIN = "statistics"
PLATFORMS = [Platform.SENSOR]
@@ -11,6 +14,12 @@ PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Statistics from a config entry."""
async_remove_stale_devices_links_keep_entity_device(
hass,
entry.entry_id,
entry.options[CONF_ENTITY_ID],
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
@@ -43,6 +43,7 @@ from homeassistant.core import (
split_entity_id,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.device import async_device_info_to_link_from_entity
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import (
async_track_point_in_utc_time,
@@ -268,6 +269,7 @@ async def async_setup_platform(
async_add_entities(
new_entities=[
StatisticsSensor(
hass=hass,
source_entity_id=config[CONF_ENTITY_ID],
name=config[CONF_NAME],
unique_id=config.get(CONF_UNIQUE_ID),
@@ -304,6 +306,7 @@ async def async_setup_entry(
async_add_entities(
[
StatisticsSensor(
hass=hass,
source_entity_id=entry.options[CONF_ENTITY_ID],
name=entry.options[CONF_NAME],
unique_id=entry.entry_id,
@@ -327,6 +330,7 @@ class StatisticsSensor(SensorEntity):
def __init__(
self,
hass: HomeAssistant,
source_entity_id: str,
name: str,
unique_id: str | None,
@@ -341,6 +345,10 @@ class StatisticsSensor(SensorEntity):
self._attr_name: str = name
self._attr_unique_id: str | None = unique_id
self._source_entity_id: str = source_entity_id
self._attr_device_info = async_device_info_to_link_from_entity(
hass,
source_entity_id,
)
self.is_binary: bool = (
split_entity_id(self._source_entity_id)[0] == BINARY_SENSOR_DOMAIN
)
+8 -9
View File
@@ -73,7 +73,7 @@ from .const import (
TYPE_HEATING,
)
from .entity import TadoZoneEntity
from .helper import decide_duration, decide_overlay_mode
from .helper import decide_duration, decide_overlay_mode, generate_supported_fanmodes
_LOGGER = logging.getLogger(__name__)
@@ -200,15 +200,14 @@ def create_climate_entity(
continue
if capabilities[mode].get("fanSpeeds"):
supported_fan_modes = [
TADO_TO_HA_FAN_MODE_MAP_LEGACY[speed]
for speed in capabilities[mode]["fanSpeeds"]
]
supported_fan_modes = generate_supported_fanmodes(
TADO_TO_HA_FAN_MODE_MAP_LEGACY, capabilities[mode]["fanSpeeds"]
)
else:
supported_fan_modes = [
TADO_TO_HA_FAN_MODE_MAP[level]
for level in capabilities[mode]["fanLevel"]
]
supported_fan_modes = generate_supported_fanmodes(
TADO_TO_HA_FAN_MODE_MAP, capabilities[mode]["fanLevel"]
)
cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"]
else:
+12
View File
@@ -49,3 +49,15 @@ def decide_duration(
)
return duration
def generate_supported_fanmodes(tado_to_ha_mapping: dict[str, str], options: list[str]):
"""Return correct list of fan modes or None."""
supported_fanmodes = [
tado_to_ha_mapping.get(option)
for option in options
if tado_to_ha_mapping.get(option) is not None
]
if not supported_fanmodes:
return None
return supported_fanmodes
@@ -8,5 +8,5 @@
"iot_class": "local_push",
"loggers": ["hatasmota"],
"mqtt": ["tasmota/discovery/#"],
"requirements": ["HATasmota==0.8.0"]
"requirements": ["HATasmota==0.9.2"]
}
+50 -33
View File
@@ -53,26 +53,10 @@ ICON = "icon"
# A Tasmota sensor type may be mapped to either a device class or an icon,
# both can only be set if the default device class icon is not appropriate
SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
hc.SENSOR_ACTIVE_ENERGYEXPORT: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ACTIVE_ENERGYIMPORT: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ACTIVE_POWERUSAGE: {
DEVICE_CLASS: SensorDeviceClass.POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_AMBIENT: {
DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_APPARENT_POWERUSAGE: {
DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_BATTERY: {
DEVICE_CLASS: SensorDeviceClass.BATTERY,
STATE_CLASS: SensorStateClass.MEASUREMENT,
@@ -92,7 +76,7 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.CURRENT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_CURRENTNEUTRAL: {
hc.SENSOR_CURRENT_NEUTRAL: {
DEVICE_CLASS: SensorDeviceClass.CURRENT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@@ -110,6 +94,34 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_EXPORT_ACTIVE: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_EXPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL},
hc.SENSOR_ENERGY_EXPORT_TARIFF: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_IMPORT_ACTIVE: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_IMPORT_REACTIVE: {STATE_CLASS: SensorStateClass.TOTAL},
hc.SENSOR_ENERGY_IMPORT_TODAY: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
hc.SENSOR_ENERGY_IMPORT_TOTAL: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_IMPORT_TOTAL_TARIFF: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_ENERGY_IMPORT_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY},
hc.SENSOR_ENERGY_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
hc.SENSOR_FREQUENCY: {
DEVICE_CLASS: SensorDeviceClass.FREQUENCY,
STATE_CLASS: SensorStateClass.MEASUREMENT,
@@ -122,6 +134,14 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.ILLUMINANCE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_POWER_ACTIVE: {
DEVICE_CLASS: SensorDeviceClass.POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_POWER_APPARENT: {
DEVICE_CLASS: SensorDeviceClass.APPARENT_POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_STATUS_IP: {ICON: "mdi:ip-network"},
hc.SENSOR_STATUS_LINK_COUNT: {ICON: "mdi:counter"},
hc.SENSOR_MOISTURE: {DEVICE_CLASS: SensorDeviceClass.MOISTURE},
@@ -144,11 +164,11 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.PM25,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_POWERFACTOR: {
hc.SENSOR_POWER_FACTOR: {
DEVICE_CLASS: SensorDeviceClass.POWER_FACTOR,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_POWERUSAGE: {
hc.SENSOR_POWER: {
DEVICE_CLASS: SensorDeviceClass.POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@@ -156,14 +176,12 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_PRESSUREATSEALEVEL: {
hc.SENSOR_PRESSURE_AT_SEA_LEVEL: {
DEVICE_CLASS: SensorDeviceClass.PRESSURE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_PROXIMITY: {ICON: "mdi:ruler"},
hc.SENSOR_REACTIVE_ENERGYEXPORT: {STATE_CLASS: SensorStateClass.TOTAL},
hc.SENSOR_REACTIVE_ENERGYIMPORT: {STATE_CLASS: SensorStateClass.TOTAL},
hc.SENSOR_REACTIVE_POWERUSAGE: {
hc.SENSOR_POWER_REACTIVE: {
DEVICE_CLASS: SensorDeviceClass.REACTIVE_POWER,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
@@ -182,15 +200,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_TODAY: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL_INCREASING,
},
hc.SENSOR_TOTAL: {
DEVICE_CLASS: SensorDeviceClass.ENERGY,
STATE_CLASS: SensorStateClass.TOTAL,
},
hc.SENSOR_TOTAL_START_TIME: {ICON: "mdi:progress-clock"},
hc.SENSOR_TVOC: {ICON: "mdi:air-filter"},
hc.SENSOR_VOLTAGE: {
DEVICE_CLASS: SensorDeviceClass.VOLTAGE,
@@ -200,7 +209,6 @@ SENSOR_DEVICE_CLASS_ICON_MAP: dict[str, dict[str, Any]] = {
DEVICE_CLASS: SensorDeviceClass.WEIGHT,
STATE_CLASS: SensorStateClass.MEASUREMENT,
},
hc.SENSOR_YESTERDAY: {DEVICE_CLASS: SensorDeviceClass.ENERGY},
}
SENSOR_UNIT_MAP = {
@@ -294,6 +302,15 @@ class TasmotaSensor(TasmotaAvailability, TasmotaDiscoveryUpdate, SensorEntity):
self._attr_native_unit_of_measurement = SENSOR_UNIT_MAP.get(
self._tasmota_entity.unit, self._tasmota_entity.unit
)
if (
self._attr_device_class is None
and self._attr_state_class is None
and self._attr_native_unit_of_measurement is None
):
# If the sensor has a numeric value, but we couldn't detect what it is,
# set state class to measurement.
if self._tasmota_entity.discovered_as_numeric:
self._attr_state_class = SensorStateClass.MEASUREMENT
async def async_added_to_hass(self) -> None:
"""Subscribe to MQTT events."""
@@ -702,7 +702,7 @@ class TelegramNotificationService:
}
if message_tag is not None:
event_data[ATTR_MESSAGE_TAG] = message_tag
if kwargs_msg[ATTR_MESSAGE_THREAD_ID] is not None:
if kwargs_msg.get(ATTR_MESSAGE_THREAD_ID) is not None:
event_data[ATTR_MESSAGE_THREAD_ID] = kwargs_msg[
ATTR_MESSAGE_THREAD_ID
]
@@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["tesla-fleet-api"],
"quality_scale": "platinum",
"requirements": ["tesla-fleet-api==0.6.1"]
"requirements": ["tesla-fleet-api==0.6.2"]
}
+38 -30
View File
@@ -6,6 +6,7 @@ import logging
from aiohttp import ClientError, ClientResponseError
from tesla_fleet_api import EnergySpecific, Tessie
from tesla_fleet_api.const import Scope
from tesla_fleet_api.exceptions import TeslaFleetError
from tessie_api import get_state_of_all_vehicles
@@ -94,41 +95,48 @@ async def async_setup_entry(hass: HomeAssistant, entry: TessieConfigEntry) -> bo
# Energy Sites
tessie = Tessie(session, api_key)
energysites: list[TessieEnergyData] = []
try:
products = (await tessie.products())["response"]
scopes = await tessie.scopes()
except TeslaFleetError as e:
raise ConfigEntryNotReady from e
energysites: list[TessieEnergyData] = []
for product in products:
if "energy_site_id" in product:
site_id = product["energy_site_id"]
api = EnergySpecific(tessie.energy, site_id)
energysites.append(
TessieEnergyData(
api=api,
id=site_id,
live_coordinator=TessieEnergySiteLiveCoordinator(hass, api),
info_coordinator=TessieEnergySiteInfoCoordinator(hass, api),
device=DeviceInfo(
identifiers={(DOMAIN, str(site_id))},
manufacturer="Tesla",
name=product.get("site_name", "Energy Site"),
),
)
)
if Scope.ENERGY_DEVICE_DATA in scopes:
try:
products = (await tessie.products())["response"]
except TeslaFleetError as e:
raise ConfigEntryNotReady from e
# Populate coordinator data before forwarding to platforms
await asyncio.gather(
*(
energysite.live_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
)
for product in products:
if "energy_site_id" in product:
site_id = product["energy_site_id"]
api = EnergySpecific(tessie.energy, site_id)
energysites.append(
TessieEnergyData(
api=api,
id=site_id,
live_coordinator=TessieEnergySiteLiveCoordinator(hass, api),
info_coordinator=TessieEnergySiteInfoCoordinator(hass, api),
device=DeviceInfo(
identifiers={(DOMAIN, str(site_id))},
manufacturer="Tesla",
name=product.get("site_name", "Energy Site"),
),
)
)
# Populate coordinator data before forwarding to platforms
await asyncio.gather(
*(
energysite.live_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
*(
energysite.info_coordinator.async_config_entry_first_refresh()
for energysite in energysites
),
)
entry.runtime_data = TessieData(vehicles, energysites)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -132,6 +132,7 @@ class TessieEnergyEntity(TessieBaseEntity):
self._attr_device_info = data.device
super().__init__(coordinator, key)
self._async_update_attrs()
class TessieWallConnectorEntity(TessieBaseEntity):
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/tessie",
"iot_class": "cloud_polling",
"loggers": ["tessie"],
"requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.1"]
"requirements": ["tessie-api==0.0.9", "tesla-fleet-api==0.6.2"]
}
+96 -42
View File
@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Iterable
from datetime import timedelta
import logging
from typing import Any
@@ -43,6 +44,7 @@ from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CREDENTIALS_HASH,
CONF_DEVICE_CONFIG,
CONNECT_TIMEOUT,
DISCOVERY_TIMEOUT,
@@ -73,6 +75,7 @@ def async_trigger_discovery(
discovered_devices: dict[str, Device],
) -> None:
"""Trigger config flows for discovered devices."""
for formatted_mac, device in discovered_devices.items():
discovery_flow.async_create_flow(
hass,
@@ -83,7 +86,6 @@ def async_trigger_discovery(
CONF_HOST: device.host,
CONF_MAC: formatted_mac,
CONF_DEVICE_CONFIG: device.config.to_dict(
credentials_hash=device.credentials_hash,
exclude_credentials=True,
),
},
@@ -133,6 +135,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
"""Set up TPLink from a config entry."""
host: str = entry.data[CONF_HOST]
credentials = await get_credentials(hass)
entry_credentials_hash = entry.data.get(CONF_CREDENTIALS_HASH)
config: DeviceConfig | None = None
if config_dict := entry.data.get(CONF_DEVICE_CONFIG):
@@ -151,19 +154,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: TPLinkConfigEntry) -> bo
config.timeout = CONNECT_TIMEOUT
if config.uses_http is True:
config.http_client = create_async_tplink_clientsession(hass)
# If we have in memory credentials use them otherwise check for credentials_hash
if credentials:
config.credentials = credentials
elif entry_credentials_hash:
config.credentials_hash = entry_credentials_hash
try:
device: Device = await Device.connect(config=config)
except AuthenticationError as ex:
# If the stored credentials_hash was used but doesn't work remove it
if not credentials and entry_credentials_hash:
data = {k: v for k, v in entry.data.items() if k != CONF_CREDENTIALS_HASH}
hass.config_entries.async_update_entry(entry, data=data)
raise ConfigEntryAuthFailed from ex
except KasaException as ex:
raise ConfigEntryNotReady from ex
device_config_dict = device.config.to_dict(
credentials_hash=device.credentials_hash, exclude_credentials=True
)
device_credentials_hash = device.credentials_hash
device_config_dict = device.config.to_dict(exclude_credentials=True)
# Do not store the credentials hash inside the device_config
device_config_dict.pop(CONF_CREDENTIALS_HASH, None)
updates: dict[str, Any] = {}
if device_credentials_hash and device_credentials_hash != entry_credentials_hash:
updates[CONF_CREDENTIALS_HASH] = device_credentials_hash
if device_config_dict != config_dict:
updates[CONF_DEVICE_CONFIG] = device_config_dict
if entry.data.get(CONF_ALIAS) != device.alias:
@@ -268,6 +283,28 @@ def mac_alias(mac: str) -> str:
return mac.replace(":", "")[-4:].upper()
def _mac_connection_or_none(device: dr.DeviceEntry) -> str | None:
return next(
(
conn
for type_, conn in device.connections
if type_ == dr.CONNECTION_NETWORK_MAC
),
None,
)
def _device_id_is_mac_or_none(mac: str, device_ids: Iterable[str]) -> str | None:
# Previously only iot devices had child devices and iot devices use
# the upper and lcase MAC addresses as device_id so match on case
# insensitive mac address as the parent device.
upper_mac = mac.upper()
return next(
(device_id for device_id in device_ids if device_id.upper() == upper_mac),
None,
)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate old entry."""
version = config_entry.version
@@ -284,49 +321,66 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
# always be linked into one device.
dev_reg = dr.async_get(hass)
for device in dr.async_entries_for_config_entry(dev_reg, config_entry.entry_id):
new_identifiers: set[tuple[str, str]] | None = None
if len(device.identifiers) > 1 and (
mac := next(
iter(
[
conn[1]
for conn in device.connections
if conn[0] == dr.CONNECTION_NETWORK_MAC
]
),
None,
original_identifiers = device.identifiers
# Get only the tplink identifier, could be tapo or other integrations.
tplink_identifiers = [
ident[1] for ident in original_identifiers if ident[0] == DOMAIN
]
# Nothing to fix if there's only one identifier. mac connection
# should never be none but if it is there's no problem.
if len(tplink_identifiers) <= 1 or not (
mac := _mac_connection_or_none(device)
):
continue
if not (
tplink_parent_device_id := _device_id_is_mac_or_none(
mac, tplink_identifiers
)
):
for identifier in device.identifiers:
# Previously only iot devices that use the MAC address as
# device_id had child devices so check for mac as the
# parent device.
if identifier[0] == DOMAIN and identifier[1].upper() == mac.upper():
new_identifiers = {identifier}
break
if new_identifiers:
dev_reg.async_update_device(
device.id, new_identifiers=new_identifiers
)
_LOGGER.debug(
"Replaced identifiers for device %s (%s): %s with: %s",
device.name,
device.model,
device.identifiers,
new_identifiers,
)
else:
# No match on mac so raise an error.
_LOGGER.error(
"Unable to replace identifiers for device %s (%s): %s",
device.name,
device.model,
device.identifiers,
)
# No match on mac so raise an error.
_LOGGER.error(
"Unable to replace identifiers for device %s (%s): %s",
device.name,
device.model,
device.identifiers,
)
continue
# Retain any identifiers for other domains
new_identifiers = {
ident for ident in device.identifiers if ident[0] != DOMAIN
}
new_identifiers.add((DOMAIN, tplink_parent_device_id))
dev_reg.async_update_device(device.id, new_identifiers=new_identifiers)
_LOGGER.debug(
"Replaced identifiers for device %s (%s): %s with: %s",
device.name,
device.model,
original_identifiers,
new_identifiers,
)
minor_version = 3
hass.config_entries.async_update_entry(config_entry, minor_version=3)
_LOGGER.debug("Migration to version %s.%s successful", version, minor_version)
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
if version == 1 and minor_version == 3:
# credentials_hash stored in the device_config should be moved to data.
updates: dict[str, Any] = {}
if config_dict := config_entry.data.get(CONF_DEVICE_CONFIG):
assert isinstance(config_dict, dict)
if credentials_hash := config_dict.pop(CONF_CREDENTIALS_HASH, None):
updates[CONF_CREDENTIALS_HASH] = credentials_hash
updates[CONF_DEVICE_CONFIG] = config_dict
minor_version = 4
hass.config_entries.async_update_entry(
config_entry,
data={
**config_entry.data,
**updates,
},
minor_version=minor_version,
)
_LOGGER.debug("Migration to version %s.%s complete", version, minor_version)
return True
+31 -12
View File
@@ -44,7 +44,13 @@ from . import (
mac_alias,
set_credentials,
)
from .const import CONF_DEVICE_CONFIG, CONNECT_TIMEOUT, DOMAIN
from .const import (
CONF_CONNECTION_TYPE,
CONF_CREDENTIALS_HASH,
CONF_DEVICE_CONFIG,
CONNECT_TIMEOUT,
DOMAIN,
)
STEP_AUTH_DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str}
@@ -55,7 +61,7 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for tplink."""
VERSION = 1
MINOR_VERSION = 3
MINOR_VERSION = 4
reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
@@ -95,9 +101,18 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
entry_config_dict = entry_data.get(CONF_DEVICE_CONFIG)
if entry_config_dict == config and entry_data[CONF_HOST] == host:
return None
updates = {**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host}
# If the connection parameters have changed the credentials_hash will be invalid.
if (
entry_config_dict
and isinstance(entry_config_dict, dict)
and entry_config_dict.get(CONF_CONNECTION_TYPE)
!= config.get(CONF_CONNECTION_TYPE)
):
updates.pop(CONF_CREDENTIALS_HASH, None)
return self.async_update_reload_and_abort(
entry,
data={**entry.data, CONF_DEVICE_CONFIG: config, CONF_HOST: host},
data=updates,
reason="already_configured",
)
@@ -345,18 +360,22 @@ class TPLinkConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def _async_create_entry_from_device(self, device: Device) -> ConfigFlowResult:
"""Create a config entry from a smart device."""
# This is only ever called after a successful device update so we know that
# the credential_hash is correct and should be saved.
self._abort_if_unique_id_configured(updates={CONF_HOST: device.host})
data = {
CONF_HOST: device.host,
CONF_ALIAS: device.alias,
CONF_MODEL: device.model,
CONF_DEVICE_CONFIG: device.config.to_dict(
exclude_credentials=True,
),
}
if device.credentials_hash:
data[CONF_CREDENTIALS_HASH] = device.credentials_hash
return self.async_create_entry(
title=f"{device.alias} {device.model}",
data={
CONF_HOST: device.host,
CONF_ALIAS: device.alias,
CONF_MODEL: device.model,
CONF_DEVICE_CONFIG: device.config.to_dict(
credentials_hash=device.credentials_hash,
exclude_credentials=True,
),
},
data=data,
)
async def _async_try_discover_and_update(
+2
View File
@@ -20,6 +20,8 @@ ATTR_TODAY_ENERGY_KWH: Final = "today_energy_kwh"
ATTR_TOTAL_ENERGY_KWH: Final = "total_energy_kwh"
CONF_DEVICE_CONFIG: Final = "device_config"
CONF_CREDENTIALS_HASH: Final = "credentials_hash"
CONF_CONNECTION_TYPE: Final = "connection_type"
PLATFORMS: Final = [
Platform.BINARY_SENSOR,
@@ -14,5 +14,5 @@
"documentation": "https://www.home-assistant.io/integrations/twinkly",
"iot_class": "local_polling",
"loggers": ["ttls"],
"requirements": ["ttls==1.5.1"]
"requirements": ["ttls==1.8.3"]
}
@@ -21,6 +21,10 @@
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine SE"
},
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine Pro Max"
}
]
}
+3 -2
View File
@@ -139,7 +139,7 @@ def async_device_uptime_value_fn(hub: UnifiHub, device: Device) -> datetime | No
@callback
def async_device_uptime_value_changed_fn(
def async_uptime_value_changed_fn(
old: StateType | date | datetime | Decimal, new: datetime | float | str | None
) -> bool:
"""Reject the new uptime value if it's too similar to the old one. Avoids unwanted fluctuation."""
@@ -310,6 +310,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
supported_fn=lambda hub, _: hub.config.option_allow_uptime_sensors,
unique_id_fn=lambda hub, obj_id: f"uptime-{obj_id}",
value_fn=async_client_uptime_value_fn,
value_changed_fn=async_uptime_value_changed_fn,
),
UnifiSensorEntityDescription[Wlans, Wlan](
key="WLAN clients",
@@ -396,7 +397,7 @@ ENTITY_DESCRIPTIONS: tuple[UnifiSensorEntityDescription, ...] = (
object_fn=lambda api, obj_id: api.devices[obj_id],
unique_id_fn=lambda hub, obj_id: f"device_uptime-{obj_id}",
value_fn=async_device_uptime_value_fn,
value_changed_fn=async_device_uptime_value_changed_fn,
value_changed_fn=async_uptime_value_changed_fn,
),
UnifiSensorEntityDescription[Devices, Device](
key="Device temperature",
@@ -426,14 +426,12 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.OCCUPANCY,
icon="mdi:doorbell-video",
ufp_required_field="feature_flags.is_doorbell",
ufp_value="is_ringing",
ufp_event_obj="last_ring_event",
),
ProtectBinaryEventEntityDescription(
key="motion",
name="Motion",
device_class=BinarySensorDeviceClass.MOTION,
ufp_value="is_motion_currently_detected",
ufp_enabled="is_motion_detection_on",
ufp_event_obj="last_motion_event",
),
@@ -40,7 +40,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["uiprotect", "unifi_discovery"],
"requirements": ["uiprotect==3.7.0", "unifi-discovery==1.1.8"],
"requirements": ["uiprotect==4.2.0", "unifi-discovery==1.2.0"],
"ssdp": [
{
"manufacturer": "Ubiquiti Networks",
@@ -53,6 +53,10 @@
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine SE"
},
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine Pro Max"
}
]
}
+1 -1
View File
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/upb",
"iot_class": "local_push",
"loggers": ["upb_lib"],
"requirements": ["upb-lib==0.5.6"]
"requirements": ["upb-lib==0.5.7"]
}
@@ -7,7 +7,7 @@
"iot_class": "local_push",
"loggers": ["aiowebostv"],
"quality_scale": "platinum",
"requirements": ["aiowebostv==0.4.0"],
"requirements": ["aiowebostv==0.4.1"],
"ssdp": [
{
"st": "urn:lge-com:service:webos-second-screen:1"
@@ -9,5 +9,5 @@
"iot_class": "cloud_push",
"loggers": ["aiowithings"],
"quality_scale": "platinum",
"requirements": ["aiowithings==3.0.1"]
"requirements": ["aiowithings==3.0.2"]
}
+8 -4
View File
@@ -305,16 +305,20 @@ async def async_send_message( # noqa: C901
timeout=timeout,
)
async def upload_file_from_path(self, path, timeout=None):
def _read_upload_file(self, path: str) -> bytes:
"""Read file from path."""
with open(path, "rb") as upfile:
_LOGGER.debug("Reading file %s", path)
return upfile.read()
async def upload_file_from_path(self, path: str, timeout=None):
"""Upload a file from a local file path via XEP_0363."""
_LOGGER.info("Uploading file from path, %s", path)
if not hass.config.is_allowed_path(path):
raise PermissionError("Could not access file. Path not allowed")
with open(path, "rb") as upfile:
_LOGGER.debug("Reading file %s", path)
input_file = upfile.read()
input_file = await hass.async_add_executor_job(self._read_upload_file, path)
filesize = len(input_file)
_LOGGER.debug("Filesize is %s bytes", filesize)
+4 -4
View File
@@ -23,12 +23,12 @@
"requirements": [
"bellows==0.39.1",
"pyserial==3.5",
"zha-quirks==0.0.116",
"zigpy-deconz==0.23.1",
"zha-quirks==0.0.117",
"zigpy-deconz==0.23.2",
"zigpy==0.64.1",
"zigpy-xbee==0.20.1",
"zigpy-zigate==0.12.0",
"zigpy-znp==0.12.1",
"zigpy-zigate==0.12.1",
"zigpy-znp==0.12.2",
"universal-silabs-flasher==0.0.20",
"pyserial-asyncio-fast==0.11"
],
+1 -1
View File
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
APPLICATION_NAME: Final = "HomeAssistant"
MAJOR_VERSION: Final = 2024
MINOR_VERSION: Final = 7
PATCH_VERSION: Final = "0.dev0"
PATCH_VERSION: Final = "0b6"
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
+9 -6
View File
@@ -158,26 +158,29 @@ class ConfigSource(enum.StrEnum):
YAML = "yaml"
class EventStateChangedData(TypedDict):
class EventStateEventData(TypedDict):
"""Base class for EVENT_STATE_CHANGED and EVENT_STATE_REPORTED data."""
entity_id: str
new_state: State | None
class EventStateChangedData(EventStateEventData):
"""EVENT_STATE_CHANGED data.
A state changed event is fired when on state write when the state is changed.
"""
entity_id: str
old_state: State | None
new_state: State | None
class EventStateReportedData(TypedDict):
class EventStateReportedData(EventStateEventData):
"""EVENT_STATE_REPORTED data.
A state reported event is fired when on state write when the state is unchanged.
"""
entity_id: str
old_last_reported: datetime.datetime
new_state: State | None
# SOURCE_* are deprecated as of Home Assistant 2022.2, use ConfigSource instead
+8
View File
@@ -297,6 +297,10 @@ SSDP = {
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine SE",
},
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine Pro Max",
},
],
"unifiprotect": [
{
@@ -311,6 +315,10 @@ SSDP = {
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine SE",
},
{
"manufacturer": "Ubiquiti Networks",
"modelDescription": "UniFi Dream Machine Pro Max",
},
],
"upnp": [
{
+5
View File
@@ -869,6 +869,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
)
add_config_entry = config_entry
if not new_connections and not new_identifiers:
raise HomeAssistantError(
"A device must have at least one of identifiers or connections"
)
if merge_connections is not UNDEFINED and new_connections is not UNDEFINED:
raise HomeAssistantError(
"Cannot define both merge_connections and new_connections"
+33 -6
View File
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Concatenate, Generic, TypeVar
from homeassistant.const import (
EVENT_CORE_CONFIG_UPDATE,
EVENT_STATE_CHANGED,
EVENT_STATE_REPORTED,
MATCH_ALL,
SUN_EVENT_SUNRISE,
SUN_EVENT_SUNSET,
@@ -26,6 +27,8 @@ from homeassistant.core import (
Event,
# Explicit reexport of 'EventStateChangedData' for backwards compatibility
EventStateChangedData as EventStateChangedData, # noqa: PLC0414
EventStateEventData,
EventStateReportedData,
HassJob,
HassJobType,
HomeAssistant,
@@ -57,6 +60,9 @@ from .typing import TemplateVarsType
_TRACK_STATE_CHANGE_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = HassKey(
"track_state_change_data"
)
_TRACK_STATE_REPORT_DATA: HassKey[_KeyedEventData[EventStateReportedData]] = HassKey(
"track_state_report_data"
)
_TRACK_STATE_ADDED_DOMAIN_DATA: HassKey[_KeyedEventData[EventStateChangedData]] = (
HassKey("track_state_added_domain_data")
)
@@ -84,6 +90,7 @@ RANDOM_MICROSECOND_MIN = 50000
RANDOM_MICROSECOND_MAX = 500000
_TypedDictT = TypeVar("_TypedDictT", bound=Mapping[str, Any])
_StateEventDataT = TypeVar("_StateEventDataT", bound=EventStateEventData)
@dataclass(slots=True, frozen=True)
@@ -324,8 +331,8 @@ def async_track_state_change_event(
@callback
def _async_dispatch_entity_id_event(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
event: Event[EventStateChangedData],
callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
event: Event[_StateEventDataT],
) -> None:
"""Dispatch to listeners."""
if not (callbacks_list := callbacks.get(event.data["entity_id"])):
@@ -342,10 +349,10 @@ def _async_dispatch_entity_id_event(
@callback
def _async_state_change_filter(
def _async_state_filter(
hass: HomeAssistant,
callbacks: dict[str, list[HassJob[[Event[EventStateChangedData]], Any]]],
event_data: EventStateChangedData,
callbacks: dict[str, list[HassJob[[Event[_StateEventDataT]], Any]]],
event_data: _StateEventDataT,
) -> bool:
"""Filter state changes by entity_id."""
return event_data["entity_id"] in callbacks
@@ -355,7 +362,7 @@ _KEYED_TRACK_STATE_CHANGE = _KeyedEventTracker(
key=_TRACK_STATE_CHANGE_DATA,
event_type=EVENT_STATE_CHANGED,
dispatcher_callable=_async_dispatch_entity_id_event,
filter_callable=_async_state_change_filter,
filter_callable=_async_state_filter,
)
@@ -372,6 +379,26 @@ def _async_track_state_change_event(
)
_KEYED_TRACK_STATE_REPORT = _KeyedEventTracker(
key=_TRACK_STATE_REPORT_DATA,
event_type=EVENT_STATE_REPORTED,
dispatcher_callable=_async_dispatch_entity_id_event,
filter_callable=_async_state_filter,
)
def async_track_state_report_event(
hass: HomeAssistant,
entity_ids: str | Iterable[str],
action: Callable[[Event[EventStateReportedData]], Any],
job_type: HassJobType | None = None,
) -> CALLBACK_TYPE:
"""Track EVENT_STATE_REPORTED by entity_id without lowercasing."""
return _async_track_event(
_KEYED_TRACK_STATE_REPORT, hass, entity_ids, action, job_type
)
@callback
def _remove_empty_listener() -> None:
"""Remove a listener that does nothing."""

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