Compare commits

...

95 Commits

Author SHA1 Message Date
Ville Skyttä 6f3a1344b3 Raise errors on interactive entity actions when suspended 2025-05-04 15:12:27 +03:00
Ville Skyttä 28f9d46f2e Mark interactive entities unavailable when suspended 2025-05-04 15:12:27 +03:00
Ville Skyttä 5156dd9c89 Normalize action parameter URL
For consistency with integration setup.
2025-05-04 15:12:26 +03:00
Ville Skyttä a159f357ae Raise errors on action call problems 2025-05-04 15:12:26 +03:00
Brett Adams 8046684179 Update models const in Teslemetry (#144175) 2025-05-04 13:44:56 +02:00
Brett Adams 5a475ec7ea Improve typing of binary sensors in Teslemetry (#144169) 2025-05-04 13:42:25 +02:00
Brett Adams 8c6edd8b81 Add better typing to Teslemetry switch platform (#144168) 2025-05-04 13:41:45 +02:00
Brett Adams de496c693e Add hazard lights binary sensor to Teslemetry (#144166) 2025-05-04 13:36:13 +02:00
Norbert Rittel cb37d4d36a Fix spelling of "comma-separated (list / event name)" in doorbird (#144190) 2025-05-04 13:09:31 +03:00
Norbert Rittel 2aa82da615 Fix spelling of "comma-separated (list)" in huawei_lte (#144189) 2025-05-04 13:09:09 +03:00
Maciej Bieniek 04982f5e12 Add missing pollen category to AccuWeather (#144185)
* Add extreme level to pollen map

* Sort

* Sort
2025-05-04 13:08:07 +03:00
Norbert Rittel b9e11b0f45 Fix spelling of "comma-separated" and "IP address" in cast (#144188) 2025-05-04 13:07:30 +03:00
hahn-th 1e0d1c46ab Bump homematicip to 2.0.1.1 (#144182)
Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-05-04 12:07:18 +02:00
Norbert Rittel b5d499dda8 Fix spelling of "comma-separated (list)" in fritzbox_callmonitor (#144191)
Also fix one missing sentence-casing in corresponding "title" string.
2025-05-04 13:06:46 +03:00
Åke Strandberg d1615f9a6e Bump pymiele to 0.4.3 (#144176)
* Use device class transation

* Bump pymiele to 0.4.3

---------

Co-authored-by: Shay Levy <levyshay1@gmail.com>
2025-05-04 11:30:37 +02:00
Marc Mueller 516a3c0504 Fix licenses check for setuptools (#144181) 2025-05-04 11:02:11 +03:00
J. Nick Koston 2a5c0d9b88 Add support for updating ESPHome deep sleep devices (#144161)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-03 20:50:17 -05:00
J. Nick Koston a15a3c12d5 Pass requestor_uuid to bond API calls (#144128) 2025-05-03 20:40:28 -04:00
J. Nick Koston a6131b3ebf Bump habluetooth to 3.48.2 (#144157) 2025-05-03 18:22:48 -05:00
Paulus Schoutsen b9aadb252f Point thumbnail TTS media source to right logo (#144162) 2025-05-03 17:21:22 -04:00
J. Nick Koston 1264c2cbfa Bump zeroconf to 0.147.0 (#144158) 2025-05-03 14:21:03 -05:00
tronikos 716b559e5d Skip the update right after the migration in Opower (#144088)
* Wait for the migration to finish in Opower

* Don't call async_block_till_done since this can timeout and seems to meant for tests

* Don't call async_block_till_done since this can timeout and seems to meant for tests
2025-05-03 15:12:01 -04:00
Charlie Rusbridger 30e4264aa9 Use kodi posters, fall back to thumbnails if unavailable. (#144066) 2025-05-03 15:10:33 -04:00
Michael fb94f8ea18 Make the network device tracking feature optional in AVM Fritz!Tools (#144149)
* make the network device tracking feature optional

* fix doc strings

* Apply suggestions from code review

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>

---------

Co-authored-by: Jan Bouwhuis <jbouwh@users.noreply.github.com>
2025-05-03 21:04:59 +02:00
Florian Sabonchi aea5760424 Fix check for locked device in AVM Fritz!SmartHome (#141697)
* feat: raise execption on hvac mode while device is locked

* fix: test for setting hvac mode while device is locked.

* feat: update translation

* feat: add separate translations for HVAC and temperature

* fix: test cases

* fix: test cases for test_set_preset_mode_boost

* rev: code review

* rev: exception string

* feat: updated  error message and added helper function

* Update homeassistant/components/fritzbox/strings.json

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

* fix: translation key

* remove check_active_or_lock_mode from async_set_preset_mode

---------

Co-authored-by: Michael <35783820+mib1185@users.noreply.github.com>
2025-05-03 20:25:27 +02:00
Jan Bouwhuis debec3bfbc Improve supported color modes description (#144144) 2025-05-03 17:13:43 +01:00
J. Diego Rodríguez Royo 4122f94fb6 Add DHCP discovery to Home Connect (#144095)
* Add DHCP discovery to Home Connect

* Added tests

* Use enums

* Use more enums
2025-05-03 17:16:02 +02:00
J. Nick Koston b48a2cf2b5 Add tests to ensure ESPHome entity_ids are preserved on upgrade (#144116) 2025-05-03 10:12:37 -05:00
Shay Levy 0ca9ad1cc0 Mark Shelly docs-data-update as done (#144151) 2025-05-03 16:17:37 +02:00
Shay Levy ee555a3700 Mark Shelly icon-translations as done (#144148) 2025-05-03 17:08:34 +03:00
Josef Zweck a2bc3e3908 Switch to common clientsession for lamarzocco (#144137) 2025-05-03 14:44:18 +02:00
Thomas55555 64b7f2c285 Improve select platform in Husqvarna Automower (#144117) 2025-05-03 15:39:46 +03:00
Marc Mueller db2435dc36 Fix litterrobot entity typing (#144147) 2025-05-03 14:35:17 +02:00
Marc Mueller 1d500fda67 Fix fritz coordinator typing (#144146) 2025-05-03 14:35:04 +02:00
Jan Bouwhuis 558b0ec3b1 Fix small issues with mqtt translations and improve readability (#144091) 2025-05-03 11:51:26 +02:00
J. Nick Koston 9780db1c22 Bump Bluetooth deps to improve auto recovery process (#144133) 2025-05-03 10:09:28 +02:00
J. Nick Koston 5e39fb6da1 Bump bleak-esphome to 2.15.1 (#144129) 2025-05-03 10:08:56 +02:00
J. Nick Koston 4450f919c3 Bump PyISY to 3.4.1 (#144127) 2025-05-02 17:46:59 -05:00
Marc Hörsken 3183bb78ff Update pywmspro to 0.2.2 to make error handling more robust (#144124) 2025-05-03 00:16:49 +02:00
J. Nick Koston e74f918382 Bump aiodns to 3.3.0 (#144115) 2025-05-02 15:53:19 -05:00
Thomas55555 247d2e7efd Bump aioautomower to 2025.5.1 (#144118) 2025-05-02 22:35:34 +02:00
Bram Kragten 32b7edb608 Update frontend to 20250502.0 (#144114) 2025-05-02 22:33:39 +02:00
Josef Zweck df4297be62 Fix intermittent unavailability for lamarzocco brew active sensor (#144120)
* Fix brew active intermittent unavailability for lamarzocco

* Whitespaces
2025-05-02 22:29:54 +02:00
Brett Adams 4c2e9fc759 Bump teslemetry-stream to 0.7.7 (#144085) 2025-05-02 21:13:12 +02:00
J. Nick Koston 2890fc7dd2 Only create a single resolver object if there are multiple aiohttp sessions (#144090) 2025-05-02 13:43:06 -05:00
Ian 97be2c4ac9 Bump py-nextbusnext to 2.1.2 (#144081)r
Bump py-nextbusnext version

Fixes #144059
2025-05-02 20:17:58 +02:00
Åke Strandberg 762d284102 Improve naming of miele freezers and fridges (#144062)
* Use device class transation

* Improve naming of miele freezers and fridges

* Address review

* Address review comment

* Simplify
2025-05-02 19:31:56 +02:00
Joost Lekkerkerker 4967c287f8 Add DHCP discovery to Knocki (#144048)
* Add DHCP discovery to Knocki

* Update homeassistant/components/knocki/quality_scale.yaml

Co-authored-by: Josef Zweck <josef@zweck.dev>

---------

Co-authored-by: Josef Zweck <josef@zweck.dev>
2025-05-02 18:34:09 +02:00
Pete Sage 5e463d6af4 bump aiokem to 0.5.9 (#144098)
fix: bump aiokem to 0.5.9
2025-05-02 17:34:58 +02:00
Åke Strandberg cbf4676ae4 Improve handling of missing miele program codes (#144093)
* Use device class transation

* Improve handling of unknown program codes

* Address review comment
2025-05-02 17:31:11 +02:00
Tomáš Bedřich 81444c8f4a Disable S3 checksums (#144092)
Disable S3 checksums (#143995)
2025-05-02 13:49:33 +02:00
J. Nick Koston 9861bd88b9 Avoid working out suggested id in entity_platform when already registered (#144079)
If the entity is already registered, avoid trying to work
out the suggested_entity_id and suggested_object_id as
async_get_or_create will discard them anyways.
2025-05-02 11:44:38 +02:00
Simone Chemelli b0f1c71129 Handle missing action exceptions in SamsungTV (#143630)
Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
2025-05-02 11:39:28 +02:00
Shay Levy 86b845f04a Mark exception-translations done in Shelly (#144073) 2025-05-02 12:32:41 +03:00
J. Diego Rodríguez Royo 3af0d6e484 Use is instead of == on check against enum value at Home Connect (#144083)
* Use `is` instead of `==` on check against enum value at Home Connect

* Revert HTTP status checks
2025-05-02 10:08:46 +02:00
epenet fca62f1ae8 Move SamsungTV test constants to fixture files (#144086) 2025-05-02 08:32:44 +02:00
Andreas Kölsch 4e8d68a2ef Fix brightness calculation when using brightness_step_pct (#143786) 2025-05-01 23:07:52 +01:00
J. Diego Rodríguez Royo 883ab44437 Move Home Connect entry state assertion at tests (#144027) 2025-05-01 23:04:03 +02:00
tronikos abd17d9af9 Pass empty set instead of empty dict to get_last_statistics (#144022) 2025-05-01 16:47:48 -04:00
J. Nick Koston a906a1754e Avoid DomainData lookup in ESPHome update platform (#144072)
We can get this from entry.runtime_data
2025-05-01 16:45:44 -04:00
Josef Zweck 255beafe08 Add connect/disconnect callbacks to lamarzocco (#144011) 2025-05-01 16:29:44 -04:00
Josef Zweck e2679004a1 Add bluetooth connection availability to diagnostics for lamarzocco (#144012)
* Add bluetooth connection availability to diagnostics for lamarzocco

* make even more detailed
2025-05-01 16:26:50 -04:00
J. Nick Koston 06bb692522 Bump inkbird-ble to 0.16.1 (#144074)
I made a mistake in one of the data lengths as I forgot to add
the length of the id which is 2 bytes. I really wish vendors
would stop putting raw data in this field.

changelog: https://github.com/Bluetooth-Devices/inkbird-ble/compare/v0.16.0...v0.16.1
2025-05-01 23:23:56 +03:00
Shay Levy 71599b8e75 Set Shelly PARALLEL_UPDATES (#144070) 2025-05-01 21:20:50 +02:00
J. Nick Koston 79f8bea48d Avoid validation of ESPHome MAC when discovered entry is ignored or unchanged (#144071)
fixes #144033
fixes #143991
2025-05-01 14:51:38 -04:00
Åke Strandberg 82b335a2c1 Flag strict typing for miele (#144060) 2025-05-01 19:10:24 +02:00
Thomas55555 361d93eb96 Remove deprecated binary sensor in Husqvarna Automower (#144064)
* Remove deprecated binary sensor in Husqvarna Automower

* snapshot
2025-05-01 18:35:48 +02:00
Ludovic BOUÉ bab699eb0c Matter Solar power fixture (#144058) 2025-05-01 17:06:08 +02:00
Thomas55555 b8881ed85b Fix test in Husqvarna Automower (#144055) 2025-05-01 16:36:05 +02:00
Åke Strandberg 4013b418dd Use device class transation for door in miele (#144053) 2025-05-01 16:33:30 +02:00
Åke Strandberg 80d714b865 Use action property defined in MieleEntity (#144052) 2025-05-01 16:06:49 +02:00
Åke Strandberg 7fcad580cb Update miele program codes and strings (#144049) 2025-05-01 15:14:11 +02:00
Ludovic BOUÉ 60b6ff4064 Matter Laundry Dryer fixture (#144043)
* Create laundry_dryer.json

* Add snapshots

* Format fixture

* Set CurrentPhase attribute

* Set OperationalState attribute

* Update snapshot
2025-05-01 14:52:32 +02:00
Josef Zweck 24252edf38 Handle TimeoutError for lamarzocco (#144042) 2025-05-01 14:02:39 +02:00
J. Diego Rodríguez Royo 79aa7aacec Sort Home Connect test params (#144035) 2025-05-01 12:04:25 +02:00
OzGav 92944fa509 Media Player strings adjust grammar (#144030) 2025-05-01 11:40:04 +02:00
J. Diego Rodríguez Royo c0f0a4a1ac Listen for an event just once at Home Connect test (#144031) 2025-05-01 11:24:29 +02:00
J. Diego Rodríguez Royo a084b9fdde Set autouse to setup_credentials Home Connect fixture (#144028) 2025-05-01 11:24:05 +02:00
Andrea Turri 83b9b8b032 Fix state of fan entity for Miele hobs with extractor when turned off (#144025) 2025-05-01 10:42:27 +02:00
J. Diego Rodríguez Royo bc47049d42 Remove non required Home Connect tests (#144024) 2025-05-01 10:18:32 +02:00
J. Diego Rodríguez Royo 17360ede28 Use common percentage const at Home Connect (#144021) 2025-05-01 09:57:42 +02:00
J. Diego Rodríguez Royo f441f4d7c0 Remove translation key for battery level in Home Connect sensor (#144020) 2025-05-01 09:57:30 +02:00
J. Diego Rodríguez Royo 5ddc449247 Remove default brightness values from Home Connect light entities (#144019) 2025-05-01 09:57:17 +02:00
J. Diego Rodríguez Royo dd8d714c94 Remove _attr_should_poll from Home Connect base entity (#144016) 2025-05-01 09:49:49 +02:00
J. Diego Rodríguez Royo c2079ddf6f Remove unused client param at Home Connect diagnostics (#144017) 2025-05-01 09:49:25 +02:00
Manu 5250590b17 Remove deprecated action api_call from Habitica integration (#143978) 2025-05-01 09:28:25 +02:00
Jan-Philipp Benecke 93f4f14b2a Default backup encryption to true when updating only location retention (#143997) 2025-04-30 22:45:41 +01:00
Ville Skyttä ba712ed514 Move huawei_lte sensor icons to icons.json where applicable (#143999) 2025-04-30 23:26:10 +02:00
Norbert Rittel 6e76ca0fb3 Add translations for "energy_distance" and "wind_direction" in random (#143994)
* Add translations for "energy_distance" and "wind_direction" in `random`

* Comma
2025-05-01 00:13:04 +03:00
Megamind b0345cce68 Bump pushover-complete to 1.2.0 (#143966)
Co-authored-by: Joostlek <joostlek@outlook.com>
2025-04-30 23:04:56 +02:00
Paulus Schoutsen c4eddc8d11 Ensure legacy TTS providers are hidden if entity exists (#143992) 2025-04-30 16:57:02 -04:00
Josef Zweck 7d89804a87 Bump pylamarzocco to 2.0.0b7 (#143989) 2025-04-30 22:56:04 +02:00
Ludovic BOUÉ b92f718e08 Matter Cooktop fixture (#143984) 2025-04-30 22:09:29 +02:00
Franck Nijhof ad0209a4a0 Bump version to 2025.6.0dev0 (#143983) 2025-04-30 21:44:19 +02:00
J. Diego Rodríguez Royo d23d25c6b7 Add units of measurement for Home Connect counter entities (#143982) 2025-04-30 21:03:17 +02:00
193 changed files with 5359 additions and 2533 deletions
+1 -1
View File
@@ -40,7 +40,7 @@ env:
CACHE_VERSION: 12
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.5"
HA_SHORT_VERSION: "2025.6"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
+1
View File
@@ -332,6 +332,7 @@ homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.met_eireann.*
homeassistant.components.metoffice.*
homeassistant.components.miele.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.minecraft_server.*
@@ -67,6 +67,7 @@ POLLEN_CATEGORY_MAP = {
2: "moderate",
3: "high",
4: "very_high",
5: "extreme",
}
UPDATE_INTERVAL_OBSERVATION = timedelta(minutes=40)
UPDATE_INTERVAL_DAILY_FORECAST = timedelta(hours=6)
@@ -72,6 +72,7 @@
"level": {
"name": "Level",
"state": {
"extreme": "Extreme",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "Moderate",
@@ -89,6 +90,7 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
@@ -123,6 +125,7 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
@@ -167,6 +170,7 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
@@ -181,6 +185,7 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
@@ -195,6 +200,7 @@
"level": {
"name": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::name%]",
"state": {
"extreme": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::extreme%]",
"high": "[%key:common::state::high%]",
"low": "[%key:common::state::low%]",
"moderate": "[%key:component::accuweather::entity::sensor::grass_pollen::state_attributes::level::state::moderate%]",
+1 -1
View File
@@ -202,7 +202,7 @@ class BackupConfig:
if agent_id not in self.data.agents:
old_agent_retention = None
self.data.agents[agent_id] = AgentConfig(
protected=agent_config.get("protected", False),
protected=agent_config.get("protected", True),
retention=new_agent_retention,
)
else:
@@ -18,9 +18,9 @@
"bleak==0.22.3",
"bleak-retry-connector==3.9.0",
"bluetooth-adapters==0.21.4",
"bluetooth-auto-recovery==1.4.5",
"bluetooth-auto-recovery==1.5.1",
"bluetooth-data-tools==1.28.1",
"dbus-fast==2.43.0",
"habluetooth==3.45.0"
"habluetooth==3.48.2"
]
}
+2 -1
View File
@@ -5,7 +5,7 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError, ClientTimeout
from bond_async import Bond, BPUPSubscriptions, start_bpup
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -49,6 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
token=token,
timeout=ClientTimeout(total=_API_TIMEOUT),
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
hub = BondHub(bond, host)
try:
+11 -3
View File
@@ -8,7 +8,7 @@ import logging
from typing import Any
from aiohttp import ClientConnectionError, ClientResponseError
from bond_async import Bond
from bond_async import Bond, RequestorUUID
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
@@ -34,7 +34,12 @@ TOKEN_SCHEMA = vol.Schema({})
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
"""Try to fetch the token from the bond device."""
bond = Bond(host, "", session=async_get_clientsession(hass))
bond = Bond(
host,
"",
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
response: dict[str, str] = {}
with contextlib.suppress(ClientConnectionError):
response = await bond.token()
@@ -45,7 +50,10 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
"""Validate the user input allows us to connect."""
bond = Bond(
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
data[CONF_HOST],
data[CONF_ACCESS_TOKEN],
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
try:
hub = BondHub(bond, data[CONF_HOST])
+2 -2
View File
@@ -10,12 +10,12 @@
"known_hosts": "Add known host"
},
"data_description": {
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
"known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working"
}
}
},
"error": {
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
"invalid_known_hosts": "Known hosts must be a comma-separated list of hosts."
}
},
"options": {
+7 -3
View File
@@ -418,9 +418,11 @@ class CloudTTSEntity(TextToSpeechEntity):
language=language,
voice=options.get(
ATTR_VOICE,
self._voice
if language == self._language
else DEFAULT_VOICES[language],
(
self._voice
if language == self._language
else DEFAULT_VOICES[language]
),
),
gender=options.get(ATTR_GENDER),
),
@@ -435,6 +437,8 @@ class CloudTTSEntity(TextToSpeechEntity):
class CloudProvider(Provider):
"""Home Assistant Cloud speech API provider."""
has_entity = True
def __init__(self, cloud: Cloud[CloudClient]) -> None:
"""Initialize cloud provider."""
self.cloud = cloud
+1 -1
View File
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/dnsip",
"iot_class": "cloud_polling",
"requirements": ["aiodns==3.2.0"]
"requirements": ["aiodns==3.3.0"]
}
@@ -3,10 +3,10 @@
"step": {
"init": {
"data": {
"events": "Comma separated list of events."
"events": "Comma-separated list of events."
},
"data_description": {
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
}
}
}
@@ -22,5 +22,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["eq3btsmart"],
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.14.0"]
"requirements": ["eq3btsmart==1.4.1", "bleak-esphome==2.15.1"]
}
@@ -22,6 +22,7 @@ import voluptuous as vol
from homeassistant.components import zeroconf
from homeassistant.config_entries import (
SOURCE_IGNORE,
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
ConfigEntry,
@@ -31,6 +32,7 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT
from homeassistant.core import callback
from homeassistant.data_entry_flow import AbortFlow
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
@@ -302,7 +304,15 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
)
):
return
if entry.source == SOURCE_IGNORE:
# Don't call _fetch_device_info() for ignored entries
raise AbortFlow("already_configured")
configured_host: str | None = entry.data.get(CONF_HOST)
configured_port: int | None = entry.data.get(CONF_PORT)
if configured_host == host and configured_port == port:
# Don't probe to verify the mac is correct since
# the host and port matches.
raise AbortFlow("already_configured")
configured_psk: str | None = entry.data.get(CONF_NOISE_PSK)
await self._fetch_device_info(host, port or configured_port, configured_psk)
updates: dict[str, Any] = {}
@@ -19,7 +19,7 @@
"requirements": [
"aioesphomeapi==30.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.14.0"
"bleak-esphome==2.15.1"
],
"zeroconf": ["_esphomelib._tcp.local."]
}
@@ -195,7 +195,10 @@
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
},
"error_uploading": {
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
},
"ota_in_progress": {
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
}
}
}
+66 -24
View File
@@ -29,7 +29,6 @@ from homeassistant.util.enum import try_parse_enum
from .const import DOMAIN
from .coordinator import ESPHomeDashboardCoordinator
from .dashboard import async_get_dashboard
from .domain_data import DomainData
from .entity import (
EsphomeEntity,
convert_api_error_ha_error,
@@ -62,7 +61,7 @@ async def async_setup_entry(
if (dashboard := async_get_dashboard(hass)) is None:
return
entry_data = DomainData.get(hass).get_entry_data(entry)
entry_data = entry.runtime_data
assert entry_data.device_info is not None
device_name = entry_data.device_info.name
unsubs: list[CALLBACK_TYPE] = []
@@ -126,21 +125,17 @@ class ESPHomeDashboardUpdateEntity(
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
}
)
self._install_lock = asyncio.Lock()
self._available_future: asyncio.Future[None] | None = None
self._update_attrs()
@callback
def _update_attrs(self) -> None:
"""Update the supported features."""
# If the device has deep sleep, we can't assume we can install updates
# as the ESP will not be connectable (by design).
coordinator = self.coordinator
device_info = self._device_info
# Install support can change at run time
if (
coordinator.last_update_success
and coordinator.supports_update
and not device_info.has_deep_sleep
):
if coordinator.last_update_success and coordinator.supports_update:
self._attr_supported_features = UpdateEntityFeature.INSTALL
else:
self._attr_supported_features = NO_FEATURES
@@ -179,6 +174,13 @@ class ESPHomeDashboardUpdateEntity(
self, static_info: list[EntityInfo] | None = None
) -> None:
"""Handle updated data from the device."""
if (
self._entry_data.available
and self._available_future
and not self._available_future.done()
):
self._available_future.set_result(None)
self._available_future = None
self._update_attrs()
self.async_write_ha_state()
@@ -193,17 +195,46 @@ class ESPHomeDashboardUpdateEntity(
entry_data.async_subscribe_device_updated(self._handle_device_update)
)
async def async_will_remove_from_hass(self) -> None:
"""Handle entity about to be removed from Home Assistant."""
if self._available_future and not self._available_future.done():
self._available_future.cancel()
self._available_future = None
async def _async_wait_available(self) -> None:
"""Wait until the device is available."""
# If the device has deep sleep, we need to wait for it to wake up
# and connect to the network to be able to install the update.
if self._entry_data.available:
return
self._available_future = self.hass.loop.create_future()
try:
await self._available_future
finally:
self._available_future = None
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
try:
if self._install_lock.locked():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="ota_in_progress",
translation_placeholders={
"configuration": self._device_info.name,
},
)
# Ensure only one OTA per device at a time
async with self._install_lock:
# Ensure only one compile at a time for ALL devices
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
if not await api.compile(configuration):
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -212,14 +243,25 @@ class ESPHomeDashboardUpdateEntity(
"configuration": configuration,
},
)
if not await api.upload(configuration, "OTA"):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
# If the device uses deep sleep, there's a small chance it goes
# to sleep right after the dashboard connects but before the OTA
# starts. In that case, the update won't go through, so we try
# again to catch it on its next wakeup.
attempts = 2 if self._device_info.has_deep_sleep else 1
try:
for attempt in range(1, attempts + 1):
await self._async_wait_available()
if await api.upload(configuration, "OTA"):
break
if attempt == attempts:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
finally:
await self.coordinator.async_request_refresh()
@@ -15,6 +15,8 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_SSL,
DOMAIN,
FRITZ_AUTH_EXCEPTIONS,
@@ -38,6 +40,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bool:
"""Set up fritzboxtools from config entry."""
_LOGGER.debug("Setting up FRITZ!Box Tools component")
avm_wrapper = AvmWrapper(
hass=hass,
config_entry=entry,
@@ -46,6 +49,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
use_tls=entry.data.get(CONF_SSL, DEFAULT_SSL),
device_discovery_enabled=entry.options.get(
CONF_FEATURE_DEVICE_TRACKING, DEFAULT_CONF_FEATURE_DEVICE_TRACKING
),
)
try:
@@ -62,6 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
raise ConfigEntryAuthFailed("Missing UPnP configuration")
await avm_wrapper.async_config_entry_first_refresh()
await avm_wrapper.async_trigger_cleanup()
entry.runtime_data = avm_wrapper
+22 -2
View File
@@ -35,7 +35,9 @@ from homeassistant.helpers.service_info.ssdp import (
from homeassistant.helpers.typing import VolDictType
from .const import (
CONF_FEATURE_DEVICE_TRACKING,
CONF_OLD_DISCOVERY,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_OLD_DISCOVERY,
DEFAULT_HOST,
DEFAULT_HTTP_PORT,
@@ -72,7 +74,8 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
"""Initialize FRITZ!Box Tools flow."""
self._name: str = ""
self._password: str = ""
self._use_tls: bool = False
self._use_tls: bool = DEFAULT_SSL
self._feature_device_discovery: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING
self._port: int | None = None
self._username: str = ""
self._model: str = ""
@@ -141,6 +144,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
options={
CONF_CONSIDER_HOME: DEFAULT_CONSIDER_HOME.total_seconds(),
CONF_OLD_DISCOVERY: DEFAULT_CONF_OLD_DISCOVERY,
CONF_FEATURE_DEVICE_TRACKING: self._feature_device_discovery,
},
)
@@ -204,6 +208,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
self._use_tls = user_input[CONF_SSL]
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
self._port = self._determine_port(user_input)
error = await self.async_fritz_tools_init()
@@ -234,6 +239,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(
CONF_FEATURE_DEVICE_TRACKING,
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
): bool,
}
),
errors=errors or {},
@@ -250,6 +259,10 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
vol.Required(
CONF_FEATURE_DEVICE_TRACKING,
default=DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
): bool,
}
),
description_placeholders={"name": self._name},
@@ -405,7 +418,7 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
return self.async_create_entry(data=user_input)
options = self.config_entry.options
data_schema = vol.Schema(
@@ -420,6 +433,13 @@ class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
CONF_OLD_DISCOVERY,
default=options.get(CONF_OLD_DISCOVERY, DEFAULT_CONF_OLD_DISCOVERY),
): bool,
vol.Optional(
CONF_FEATURE_DEVICE_TRACKING,
default=options.get(
CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
),
): bool,
}
)
return self.async_show_form(step_id="init", data_schema=data_schema)
+3
View File
@@ -40,6 +40,9 @@ PLATFORMS = [
CONF_OLD_DISCOVERY = "old_discovery"
DEFAULT_CONF_OLD_DISCOVERY = False
CONF_FEATURE_DEVICE_TRACKING = "feature_device_tracking"
DEFAULT_CONF_FEATURE_DEVICE_TRACKING = True
DSL_CONNECTION: Literal["dsl"] = "dsl"
DEFAULT_DEVICE_NAME = "Unknown device"
+23 -9
View File
@@ -39,6 +39,7 @@ from homeassistant.util.hass_dict import HassKey
from .const import (
CONF_OLD_DISCOVERY,
DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
DEFAULT_CONF_OLD_DISCOVERY,
DEFAULT_HOST,
DEFAULT_SSL,
@@ -175,6 +176,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
username: str = DEFAULT_USERNAME,
host: str = DEFAULT_HOST,
use_tls: bool = DEFAULT_SSL,
device_discovery_enabled: bool = DEFAULT_CONF_FEATURE_DEVICE_TRACKING,
) -> None:
"""Initialize FritzboxTools class."""
super().__init__(
@@ -202,6 +204,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self.port = port
self.username = username
self.use_tls = use_tls
self.device_discovery_enabled = device_discovery_enabled
self.has_call_deflections: bool = False
self._model: str | None = None
self._current_firmware: str | None = None
@@ -332,10 +335,15 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
"entity_states": {},
}
try:
await self.async_scan_devices()
await self.async_update_device_info()
if self.device_discovery_enabled:
await self.async_scan_devices()
entity_data["entity_states"] = await self.hass.async_add_executor_job(
self._entity_states_update
)
if self.has_call_deflections:
entity_data[
"call_deflections"
@@ -521,7 +529,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
return {}
def manage_device_info(
self, dev_info: Device, dev_mac: str, consider_home: bool
self, dev_info: Device, dev_mac: str, consider_home: float
) -> bool:
"""Update device lists and return if device is new."""
_LOGGER.debug("Client dev_info: %s", dev_info)
@@ -551,12 +559,8 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
if new_device:
async_dispatcher_send(self.hass, self.signal_device_new)
async def async_scan_devices(self, now: datetime | None = None) -> None:
"""Scan for new devices and return a list of found device ids."""
if self.hass.is_stopping:
_ha_is_stopping("scan devices")
return
async def async_update_device_info(self, now: datetime | None = None) -> None:
"""Update own device information."""
_LOGGER.debug("Checking host info for FRITZ!Box device %s", self.host)
(
@@ -565,6 +569,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
self._release_url,
) = await self._async_update_device_info()
async def async_scan_devices(self, now: datetime | None = None) -> None:
"""Scan for new network devices."""
if self.hass.is_stopping:
_ha_is_stopping("scan devices")
return
_LOGGER.debug("Checking devices for FRITZ!Box device %s", self.host)
_default_consider_home = DEFAULT_CONSIDER_HOME.total_seconds()
if self._options:
@@ -683,7 +694,10 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
async def async_trigger_cleanup(self) -> None:
"""Trigger device trackers cleanup."""
device_hosts = await self._async_update_hosts_info()
_LOGGER.debug("Device tracker cleanup triggered")
device_hosts = {self.mac: Device(True, "", "", "", "", None)}
if self.device_discovery_enabled:
device_hosts = await self._async_update_hosts_info()
entity_reg: er.EntityRegistry = er.async_get(self.hass)
config_entry = self.config_entry
+15 -7
View File
@@ -4,7 +4,9 @@
"data_description_port": "Leave empty to use the default port.",
"data_description_username": "Username for the FRITZ!Box.",
"data_description_password": "Password for the FRITZ!Box.",
"data_description_ssl": "Use SSL to connect to the FRITZ!Box."
"data_description_ssl": "Use SSL to connect to the FRITZ!Box.",
"data_description_feature_device_tracking": "Enable or disable the network device tracking feature.",
"data_feature_device_tracking": "Enable network device tracking"
},
"config": {
"flow_title": "{name}",
@@ -15,12 +17,14 @@
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]"
"ssl": "[%key:common::config_flow::data::ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
},
"data_description": {
"username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
}
},
"reauth_confirm": {
@@ -57,14 +61,16 @@
"port": "[%key:common::config_flow::data::port%]",
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]",
"ssl": "[%key:common::config_flow::data::ssl%]"
"ssl": "[%key:common::config_flow::data::ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
},
"data_description": {
"host": "[%key:component::fritz::common::data_description_host%]",
"port": "[%key:component::fritz::common::data_description_port%]",
"username": "[%key:component::fritz::common::data_description_username%]",
"password": "[%key:component::fritz::common::data_description_password%]",
"ssl": "[%key:component::fritz::common::data_description_ssl%]"
"ssl": "[%key:component::fritz::common::data_description_ssl%]",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
}
}
},
@@ -89,11 +95,13 @@
"init": {
"data": {
"consider_home": "Seconds to consider a device at 'home'",
"old_discovery": "Enable old discovery method"
"old_discovery": "Enable old discovery method",
"feature_device_tracking": "[%key:component::fritz::common::data_feature_device_tracking%]"
},
"data_description": {
"consider_home": "Time in seconds to consider a device at home. Default is 180 seconds.",
"old_discovery": "Enable old discovery method. This is needed for some scenarios."
"old_discovery": "Enable old discovery method. This is needed for some scenarios.",
"feature_device_tracking": "[%key:component::fritz::common::data_description_feature_device_tracking%]"
}
}
}
+17 -10
View File
@@ -144,6 +144,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
self.check_active_or_lock_mode()
if kwargs.get(ATTR_HVAC_MODE) is HVACMode.OFF:
await self.async_set_hkr_state("off")
elif (target_temp := kwargs.get(ATTR_TEMPERATURE)) is not None:
@@ -168,11 +169,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new operation mode."""
if self.data.holiday_active or self.data.summer_active:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_hvac_while_active_mode",
)
self.check_active_or_lock_mode()
if self.hvac_mode is hvac_mode:
LOGGER.debug(
"%s is already in requested hvac mode %s", self.name, hvac_mode
@@ -204,11 +201,7 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
if self.data.holiday_active or self.data.summer_active:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_preset_while_active_mode",
)
self.check_active_or_lock_mode()
await self.async_set_hkr_state(PRESET_API_HKR_STATE_MAPPING[preset_mode])
@property
@@ -230,3 +223,17 @@ class FritzboxThermostat(FritzBoxDeviceEntity, ClimateEntity):
attrs[ATTR_STATE_WINDOW_OPEN] = self.data.window_open
return attrs
def check_active_or_lock_mode(self) -> None:
"""Check if in summer/vacation mode or lock enabled."""
if self.data.holiday_active or self.data.summer_active:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_settings_while_active_mode",
)
if self.data.lock:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="change_settings_while_lock_enabled",
)
@@ -88,11 +88,11 @@
"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."
"change_settings_while_lock_enabled": {
"message": "Can't change settings while manual access for telephone, app, or user interface is disabled on the device"
},
"change_hvac_while_active_mode": {
"message": "Can't change HVAC mode while holiday or summer mode is active on the device."
"change_settings_while_active_mode": {
"message": "Can't change settings while holiday or summer mode is active on the device."
}
}
}
@@ -39,9 +39,9 @@
"options": {
"step": {
"init": {
"title": "Configure Prefixes",
"title": "Configure prefixes",
"data": {
"prefixes": "Prefixes (comma separated list)"
"prefixes": "Prefixes (comma-separated list)"
}
}
},
@@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250430.2"]
"requirements": ["home-assistant-frontend==20250502.0"]
}
+1 -10
View File
@@ -1,6 +1,6 @@
"""Constants for the habitica integration."""
from homeassistant.const import APPLICATION_NAME, CONF_PATH, __version__
from homeassistant.const import APPLICATION_NAME, __version__
CONF_API_USER = "api_user"
@@ -13,15 +13,6 @@ HABITICANS_URL = "https://habitica.com/static/img/home-main@3x.ffc32b12.png"
DOMAIN = "habitica"
# service constants
SERVICE_API_CALL = "api_call"
ATTR_PATH = CONF_PATH
ATTR_ARGS = "args"
# event constants
EVENT_API_CALL_SUCCESS = f"{DOMAIN}_{SERVICE_API_CALL}_success"
ATTR_DATA = "data"
MANUFACTURER = "HabitRPG, Inc."
NAME = "Habitica"
+1 -61
View File
@@ -29,7 +29,7 @@ import voluptuous as vol
from homeassistant.components.todo import ATTR_RENAME
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_DATE, ATTR_NAME, CONF_NAME
from homeassistant.const import ATTR_DATE, ATTR_NAME
from homeassistant.core import (
HomeAssistant,
ServiceCall,
@@ -38,28 +38,24 @@ from homeassistant.core import (
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.selector import ConfigEntrySelector
from homeassistant.util import dt as dt_util
from .const import (
ATTR_ADD_CHECKLIST_ITEM,
ATTR_ALIAS,
ATTR_ARGS,
ATTR_CLEAR_DATE,
ATTR_CLEAR_REMINDER,
ATTR_CONFIG_ENTRY,
ATTR_COST,
ATTR_COUNTER_DOWN,
ATTR_COUNTER_UP,
ATTR_DATA,
ATTR_DIRECTION,
ATTR_FREQUENCY,
ATTR_INTERVAL,
ATTR_ITEM,
ATTR_KEYWORD,
ATTR_NOTES,
ATTR_PATH,
ATTR_PRIORITY,
ATTR_REMINDER,
ATTR_REMOVE_CHECKLIST_ITEM,
@@ -78,10 +74,8 @@ from .const import (
ATTR_UNSCORE_CHECKLIST_ITEM,
ATTR_UP_DOWN,
DOMAIN,
EVENT_API_CALL_SUCCESS,
SERVICE_ABORT_QUEST,
SERVICE_ACCEPT_QUEST,
SERVICE_API_CALL,
SERVICE_CANCEL_QUEST,
SERVICE_CAST_SKILL,
SERVICE_CREATE_DAILY,
@@ -106,14 +100,6 @@ from .coordinator import HabiticaConfigEntry
_LOGGER = logging.getLogger(__name__)
SERVICE_API_CALL_SCHEMA = vol.Schema(
{
vol.Required(ATTR_NAME): str,
vol.Required(ATTR_PATH): vol.All(cv.ensure_list, [str]),
vol.Optional(ATTR_ARGS): dict,
}
)
SERVICE_CAST_SKILL_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector({"integration": DOMAIN}),
@@ -266,46 +252,6 @@ def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
"""Set up services for Habitica integration."""
async def handle_api_call(call: ServiceCall) -> None:
async_create_issue(
hass,
DOMAIN,
"deprecated_api_call",
breaks_in_ha_version="2025.6.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_api_call",
)
_LOGGER.warning(
"Deprecated action called: 'habitica.api_call' is deprecated and will be removed in Home Assistant version 2025.6.0"
)
name = call.data[ATTR_NAME]
path = call.data[ATTR_PATH]
entries: list[HabiticaConfigEntry] = hass.config_entries.async_entries(DOMAIN)
api = None
for entry in entries:
if entry.data[CONF_NAME] == name:
api = await entry.runtime_data.habitica.habitipy()
break
if api is None:
_LOGGER.error("API_CALL: User '%s' not configured", name)
return
try:
for element in path:
api = api[element]
except KeyError:
_LOGGER.error(
"API_CALL: Path %s is invalid for API on '{%s}' element", path, element
)
return
kwargs = call.data.get(ATTR_ARGS, {})
data = await api(**kwargs)
hass.bus.async_fire(
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
)
async def cast_skill(call: ServiceCall) -> ServiceResponse:
"""Skill action."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
@@ -928,12 +874,6 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
schema=SERVICE_CREATE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
handle_api_call,
schema=SERVICE_API_CALL_SCHEMA,
)
hass.services.async_register(
DOMAIN,
@@ -1,20 +1,4 @@
# Describes the format for Habitica service
api_call:
fields:
name:
required: true
example: "xxxNotAValidNickxxx"
selector:
text:
path:
required: true
example: '["tasks", "user", "post"]'
selector:
object:
args:
example: '{"text": "Use API from Home Assistant", "type": "todo"}'
selector:
object:
cast_skill:
fields:
config_entry: &config_entry
@@ -526,31 +526,9 @@
"deprecated_entity": {
"title": "The Habitica {name} entity is deprecated",
"description": "The Habitica entity `{entity}` is deprecated and will be removed in a future release.\nPlease update your automations and scripts, disable `{entity}` and reload the integration/restart Home Assistant to fix this issue."
},
"deprecated_api_call": {
"title": "The Habitica action habitica.api_call is deprecated",
"description": "The Habitica action `habitica.api_call` is deprecated and will be removed in Home Assistant 2025.5.0.\n\nPlease update your automations and scripts to use other Habitica actions and entities."
}
},
"services": {
"api_call": {
"name": "API name",
"description": "Calls Habitica API.",
"fields": {
"name": {
"name": "[%key:common::config_flow::data::name%]",
"description": "Habitica's username to call for."
},
"path": {
"name": "[%key:common::config_flow::data::path%]",
"description": "Items from API URL in form of an array with method attached at the end. Consult https://habitica.com/apidoc/. Example uses https://habitica.com/apidoc/#api-Task-CreateUserTasks."
},
"args": {
"name": "Args",
"description": "Any additional JSON or URL parameter arguments. See apidoc mentioned for path. Example uses same API endpoint."
}
}
},
"cast_skill": {
"name": "Cast a skill",
"description": "Uses a skill or spell from your Habitica character on a specific task to affect its progress or status.",
@@ -4,8 +4,6 @@ from __future__ import annotations
from typing import Any
from aiohomeconnect.client import Client as HomeConnectClient
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntry
@@ -14,7 +12,7 @@ from .coordinator import HomeConnectApplianceData, HomeConnectConfigEntry
async def _generate_appliance_diagnostics(
client: HomeConnectClient, appliance: HomeConnectApplianceData
appliance: HomeConnectApplianceData,
) -> dict[str, Any]:
return {
**appliance.info.to_dict(),
@@ -31,9 +29,7 @@ async def async_get_config_entry_diagnostics(
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
return {
appliance.info.ha_id: await _generate_appliance_diagnostics(
entry.runtime_data.client, appliance
)
appliance.info.ha_id: await _generate_appliance_diagnostics(appliance)
for appliance in entry.runtime_data.data.values()
}
@@ -45,6 +41,4 @@ async def async_get_device_diagnostics(
ha_id = next(
(identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN),
)
return await _generate_appliance_diagnostics(
entry.runtime_data.client, entry.runtime_data.data[ha_id]
)
return await _generate_appliance_diagnostics(entry.runtime_data.data[ha_id])
@@ -32,7 +32,6 @@ _LOGGER = logging.getLogger(__name__)
class HomeConnectEntity(CoordinatorEntity[HomeConnectCoordinator]):
"""Generic Home Connect entity (base class)."""
_attr_should_poll = False
_attr_has_entity_name = True
def __init__(
@@ -39,11 +39,11 @@ PARALLEL_UPDATES = 1
class HomeConnectLightEntityDescription(LightEntityDescription):
"""Light entity description."""
brightness_key: SettingKey | None = None
brightness_key: SettingKey
brightness_scale: tuple[float, float]
color_key: SettingKey | None = None
enable_custom_color_value_key: str | None = None
custom_color_key: SettingKey | None = None
brightness_scale: tuple[float, float] = (0.0, 100.0)
LIGHTS: tuple[HomeConnectLightEntityDescription, ...] = (
@@ -4,6 +4,20 @@
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
"config_flow": true,
"dependencies": ["application_credentials", "repairs"],
"dhcp": [
{
"hostname": "balay-*",
"macaddress": "C8D778*"
},
{
"hostname": "(bosch|siemens)-*",
"macaddress": "68A40E*"
},
{
"hostname": "siemens-*",
"macaddress": "38B4D3*"
}
],
"documentation": "https://www.home-assistant.io/integrations/home_connect",
"iot_class": "cloud_push",
"loggers": ["aiohomeconnect"],
@@ -11,6 +11,7 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -79,7 +80,7 @@ NUMBERS = (
NumberEntityDescription(
key=SettingKey.COOKING_HOOD_COLOR_TEMPERATURE_PERCENT,
translation_key="color_temperature_percent",
native_unit_of_measurement="%",
native_unit_of_measurement=PERCENTAGE,
),
NumberEntityDescription(
key=SettingKey.LAUNDRY_CARE_WASHER_I_DOS_1_BASE_LEVEL,
@@ -159,7 +159,6 @@ SENSORS = (
HomeConnectSensorEntityDescription(
key=StatusKey.BSH_COMMON_BATTERY_LEVEL,
device_class=SensorDeviceClass.BATTERY,
translation_key="battery_level",
),
HomeConnectSensorEntityDescription(
key=StatusKey.BSH_COMMON_VIDEO_CAMERA_STATE,
@@ -1551,34 +1551,39 @@
}
},
"coffee_counter": {
"name": "Coffees"
"name": "Coffees",
"unit_of_measurement": "coffees"
},
"powder_coffee_counter": {
"name": "Powder coffees"
"name": "Powder coffees",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::coffee_counter::unit_of_measurement%]"
},
"hot_water_counter": {
"name": "Hot water"
},
"hot_water_cups_counter": {
"name": "Hot water cups"
"name": "Hot water cups",
"unit_of_measurement": "cups"
},
"hot_milk_counter": {
"name": "Hot milk cups"
"name": "Hot milk cups",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
},
"frothy_milk_counter": {
"name": "Frothy milk cups"
"name": "Frothy milk cups",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
},
"milk_counter": {
"name": "Milk cups"
"name": "Milk cups",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
},
"coffee_and_milk_counter": {
"name": "Coffee and milk cups"
"name": "Coffee and milk cups",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
},
"ristretto_espresso_counter": {
"name": "Ristretto espresso cups"
},
"battery_level": {
"name": "Battery level"
"name": "Ristretto espresso cups",
"unit_of_measurement": "[%key:component::home_connect::entity::sensor::hot_water_cups_counter::unit_of_measurement%]"
},
"camera_state": {
"name": "Camera state",
@@ -79,7 +79,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
automations = automations_with_entity(self.hass, self.entity_id)
scripts = scripts_with_entity(self.hass, self.entity_id)
items = automations + scripts
@@ -123,7 +123,7 @@ class HomeConnectTimeEntity(HomeConnectEntity, TimeEntity):
async def async_will_remove_from_hass(self) -> None:
"""Call when entity will be removed from hass."""
if self.bsh_key == SettingKey.BSH_COMMON_ALARM_CLOCK:
if self.bsh_key is SettingKey.BSH_COMMON_ALARM_CLOCK:
async_delete_issue(
self.hass,
DOMAIN,
@@ -9,10 +9,10 @@ from typing import Any
from homematicip.async_home import AsyncHome
from homematicip.auth import Auth
from homematicip.base.base_connection import HmipConnectionError
from homematicip.base.enums import EventType
from homematicip.connection.connection_context import ConnectionContextBuilder
from homematicip.connection.rest_connection import RestConnection
from homematicip.exceptions.connection_exceptions import HmipConnectionError
import homeassistant
from homeassistant.config_entries import ConfigEntry
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.0.1"]
"requirements": ["homematicip==2.0.1.1"]
}
+17 -13
View File
@@ -21,6 +21,7 @@ from huawei_lte_api.exceptions import (
ResponseErrorNotSupportedException,
)
from requests.exceptions import Timeout
from url_normalize import url_normalize
import voluptuous as vol
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
@@ -40,7 +41,11 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
@@ -121,7 +126,7 @@ CONFIG_SCHEMA = vol.Schema(
extra=vol.ALLOW_EXTRA,
)
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.url})
SERVICE_SCHEMA = vol.Schema({vol.Optional(CONF_URL): cv.string})
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -507,26 +512,22 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
because the latter is not available anywhere in the UI.
"""
routers = hass.data[DOMAIN].routers
if url := service.data.get(CONF_URL):
if url := url_normalize(service.data.get(CONF_URL), default_scheme="http"):
router = next(
(router for router in routers.values() if router.url == url), None
)
elif not routers:
_LOGGER.error("%s: no routers configured", service.service)
return
raise ServiceValidationError("No routers configured")
elif len(routers) == 1:
router = next(iter(routers.values()))
else:
_LOGGER.error(
"%s: more than one router configured, must specify one of URLs %s",
service.service,
sorted(router.url for router in routers.values()),
raise ServiceValidationError(
f"More than one router configured, must specify one of URLs {sorted(router.url for router in routers.values())}"
)
return
if not router:
_LOGGER.error("%s: router %s unavailable", service.service, url)
return
raise ServiceValidationError(f"Router {url} not available")
was_suspended = router.suspended
if service.service == SERVICE_RESUME_INTEGRATION:
# Login will be handled automatically on demand
router.suspended = False
@@ -536,7 +537,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
router.suspended = True
_LOGGER.debug("%s: %s", service.service, "done")
else:
_LOGGER.error("%s: unsupported service", service.service)
raise ServiceValidationError(f"Unsupported service {service.service}")
if was_suspended != router.suspended:
# Make interactive entities' availability update
dispatcher_send(hass, UPDATE_SIGNAL, router.config_entry.unique_id)
for service in ADMIN_SERVICES:
async_register_admin_service(
@@ -14,10 +14,11 @@ from homeassistant.components.button import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import entity_platform
from .const import DOMAIN
from .entity import HuaweiLteBaseEntityWithDevice
from .entity import HuaweiLteBaseInteractiveEntity
_LOGGER = logging.getLogger(__name__)
@@ -36,7 +37,7 @@ async def async_setup_entry(
async_add_entities(buttons)
class BaseButton(HuaweiLteBaseEntityWithDevice, ButtonEntity):
class BaseButton(HuaweiLteBaseInteractiveEntity, ButtonEntity):
"""Huawei LTE button base class."""
@property
@@ -50,10 +51,7 @@ class BaseButton(HuaweiLteBaseEntityWithDevice, ButtonEntity):
def press(self) -> None:
"""Press button."""
if self.router.suspended:
_LOGGER.debug(
"%s: ignored, integration suspended", self.entity_description.key
)
return
raise ServiceValidationError("Integration is suspended")
result = self._press()
_LOGGER.debug("%s: %s", self.entity_description.key, result)
@@ -66,3 +66,12 @@ class HuaweiLteBaseEntityWithDevice(HuaweiLteBaseEntity):
connections=self.router.device_connections,
identifiers=self.router.device_identifiers,
)
class HuaweiLteBaseInteractiveEntity(HuaweiLteBaseEntityWithDevice):
"""Base interactive entity."""
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and not self.router.suspended
@@ -37,6 +37,137 @@
"default": "mdi:antenna"
}
},
"sensor": {
"uptime": {
"default": "mdi:timer-outline"
},
"wan_ip_address": {
"default": "mdi:ip"
},
"wan_ipv6_address": {
"default": "mdi:ip"
},
"cell_id": {
"default": "mdi:antenna"
},
"cqi0": {
"default": "mdi:speedometer"
},
"cqi1": {
"default": "mdi:speedometer"
},
"enodeb_id": {
"default": "mdi:antenna"
},
"lac": {
"default": "mdi:map-marker"
},
"nei_cellid": {
"default": "mdi:antenna"
},
"nrcqi0": {
"default": "mdi:speedometer"
},
"nrcqi1": {
"default": "mdi:speedometer"
},
"pci": {
"default": "mdi:antenna"
},
"rac": {
"default": "mdi:map-marker"
},
"tac": {
"default": "mdi:map-marker"
},
"sms_unread": {
"default": "mdi:email-arrow-left"
},
"current_day_transfer": {
"default": "mdi:arrow-up-down-bold"
},
"current_month_download": {
"default": "mdi:download"
},
"current_month_upload": {
"default": "mdi:upload"
},
"wifi_clients_connected": {
"default": "mdi:wifi"
},
"primary_dns_server": {
"default": "mdi:ip"
},
"primary_ipv6_dns_server": {
"default": "mdi:ip"
},
"secondary_dns_server": {
"default": "mdi:ip"
},
"secondary_ipv6_dns_server": {
"default": "mdi:ip"
},
"current_connection_duration": {
"default": "mdi:timer-outline"
},
"current_connection_download": {
"default": "mdi:download"
},
"current_download_rate": {
"default": "mdi:download"
},
"current_connection_upload": {
"default": "mdi:upload"
},
"current_upload_rate": {
"default": "mdi:upload"
},
"total_connected_duration": {
"default": "mdi:timer-outline"
},
"total_download": {
"default": "mdi:download"
},
"total_upload": {
"default": "mdi:upload"
},
"sms_deleted_device": {
"default": "mdi:email-minus"
},
"sms_drafts_device": {
"default": "mdi:email-arrow-right-outline"
},
"sms_inbox_device": {
"default": "mdi:email"
},
"sms_capacity_device": {
"default": "mdi:email"
},
"sms_outbox_device": {
"default": "mdi:email-arrow-right"
},
"sms_unread_device": {
"default": "mdi:email-arrow-left"
},
"sms_drafts_sim": {
"default": "mdi:email-arrow-right-outline"
},
"sms_inbox_sim": {
"default": "mdi:email"
},
"sms_capacity_sim": {
"default": "mdi:email"
},
"sms_outbox_sim": {
"default": "mdi:email-arrow-right"
},
"sms_unread_sim": {
"default": "mdi:email-arrow-left"
},
"sms_messages_sim": {
"default": "mdi:email-arrow-left"
}
},
"switch": {
"mobile_data": {
"default": "mdi:signal-off",
@@ -17,13 +17,14 @@ from homeassistant.components.select import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import UNDEFINED
from . import Router
from .const import DOMAIN, KEY_NET_NET_MODE
from .entity import HuaweiLteBaseEntityWithDevice
from .entity import HuaweiLteBaseInteractiveEntity
_LOGGER = logging.getLogger(__name__)
@@ -76,7 +77,7 @@ async def async_setup_entry(
async_add_entities(selects, True)
class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity):
class HuaweiLteSelectEntity(HuaweiLteBaseInteractiveEntity, SelectEntity):
"""Huawei LTE select entity."""
entity_description: HuaweiSelectEntityDescription
@@ -102,6 +103,8 @@ class HuaweiLteSelectEntity(HuaweiLteBaseEntityWithDevice, SelectEntity):
def select_option(self, option: str) -> None:
"""Change the selected option."""
if self.router.suspended:
raise ServiceValidationError("Integration is suspended")
self.entity_description.setter_fn(option)
@property
+1 -44
View File
@@ -138,7 +138,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"uptime": HuaweiSensorEntityDescription(
key="uptime",
translation_key="uptime",
icon="mdi:timer-outline",
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
entity_category=EntityCategory.DIAGNOSTIC,
@@ -146,14 +145,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"WanIPAddress": HuaweiSensorEntityDescription(
key="WanIPAddress",
translation_key="wan_ip_address",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=True,
),
"WanIPv6Address": HuaweiSensorEntityDescription(
key="WanIPv6Address",
translation_key="wan_ipv6_address",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
),
},
@@ -181,19 +178,16 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"cell_id": HuaweiSensorEntityDescription(
key="cell_id",
translation_key="cell_id",
icon="mdi:antenna",
entity_category=EntityCategory.DIAGNOSTIC,
),
"cqi0": HuaweiSensorEntityDescription(
key="cqi0",
translation_key="cqi0",
icon="mdi:speedometer",
entity_category=EntityCategory.DIAGNOSTIC,
),
"cqi1": HuaweiSensorEntityDescription(
key="cqi1",
translation_key="cqi1",
icon="mdi:speedometer",
entity_category=EntityCategory.DIAGNOSTIC,
),
"dl_mcs": HuaweiSensorEntityDescription(
@@ -230,7 +224,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"enodeb_id": HuaweiSensorEntityDescription(
key="enodeb_id",
translation_key="enodeb_id",
icon="mdi:antenna",
entity_category=EntityCategory.DIAGNOSTIC,
),
"ims": HuaweiSensorEntityDescription(
@@ -241,7 +234,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"lac": HuaweiSensorEntityDescription(
key="lac",
translation_key="lac",
icon="mdi:map-marker",
entity_category=EntityCategory.DIAGNOSTIC,
),
"ltedlfreq": HuaweiSensorEntityDescription(
@@ -279,7 +271,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"nei_cellid": HuaweiSensorEntityDescription(
key="nei_cellid",
translation_key="nei_cellid",
icon="mdi:antenna",
entity_category=EntityCategory.DIAGNOSTIC,
),
"nrbler": HuaweiSensorEntityDescription(
@@ -290,13 +281,11 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"nrcqi0": HuaweiSensorEntityDescription(
key="nrcqi0",
translation_key="nrcqi0",
icon="mdi:speedometer",
entity_category=EntityCategory.DIAGNOSTIC,
),
"nrcqi1": HuaweiSensorEntityDescription(
key="nrcqi1",
translation_key="nrcqi1",
icon="mdi:speedometer",
entity_category=EntityCategory.DIAGNOSTIC,
),
"nrdlbandwidth": HuaweiSensorEntityDescription(
@@ -376,7 +365,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"pci": HuaweiSensorEntityDescription(
key="pci",
translation_key="pci",
icon="mdi:antenna",
entity_category=EntityCategory.DIAGNOSTIC,
),
"plmn": HuaweiSensorEntityDescription(
@@ -387,7 +375,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"rac": HuaweiSensorEntityDescription(
key="rac",
translation_key="rac",
icon="mdi:map-marker",
entity_category=EntityCategory.DIAGNOSTIC,
),
"rrc_status": HuaweiSensorEntityDescription(
@@ -458,7 +445,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"tac": HuaweiSensorEntityDescription(
key="tac",
translation_key="tac",
icon="mdi:map-marker",
entity_category=EntityCategory.DIAGNOSTIC,
),
"tdd": HuaweiSensorEntityDescription(
@@ -522,7 +508,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"UnreadMessage": HuaweiSensorEntityDescription(
key="UnreadMessage",
translation_key="sms_unread",
icon="mdi:email-arrow-left",
),
},
),
@@ -536,7 +521,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_day_transfer",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:arrow-up-down-bold",
state_class=SensorStateClass.TOTAL,
last_reset_item="CurrentDayDuration",
last_reset_format_fn=format_last_reset_elapsed_seconds,
@@ -546,7 +530,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_month_download",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
state_class=SensorStateClass.TOTAL,
last_reset_item="MonthDuration",
last_reset_format_fn=format_last_reset_elapsed_seconds,
@@ -556,7 +539,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_month_upload",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:upload",
state_class=SensorStateClass.TOTAL,
last_reset_item="MonthDuration",
last_reset_format_fn=format_last_reset_elapsed_seconds,
@@ -580,32 +562,27 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"CurrentWifiUser": HuaweiSensorEntityDescription(
key="CurrentWifiUser",
translation_key="wifi_clients_connected",
icon="mdi:wifi",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
"PrimaryDns": HuaweiSensorEntityDescription(
key="PrimaryDns",
translation_key="primary_dns_server",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
),
"PrimaryIPv6Dns": HuaweiSensorEntityDescription(
key="PrimaryIPv6Dns",
translation_key="primary_ipv6_dns_server",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
),
"SecondaryDns": HuaweiSensorEntityDescription(
key="SecondaryDns",
translation_key="secondary_dns_server",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
),
"SecondaryIPv6Dns": HuaweiSensorEntityDescription(
key="SecondaryIPv6Dns",
translation_key="secondary_ipv6_dns_server",
icon="mdi:ip",
entity_category=EntityCategory.DIAGNOSTIC,
),
},
@@ -618,14 +595,12 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_connection_duration",
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
icon="mdi:timer-outline",
),
"CurrentDownload": HuaweiSensorEntityDescription(
key="CurrentDownload",
translation_key="current_connection_download",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
state_class=SensorStateClass.TOTAL_INCREASING,
),
"CurrentDownloadRate": HuaweiSensorEntityDescription(
@@ -633,7 +608,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_download_rate",
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
icon="mdi:download",
state_class=SensorStateClass.MEASUREMENT,
),
"CurrentUpload": HuaweiSensorEntityDescription(
@@ -641,7 +615,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_connection_upload",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:upload",
state_class=SensorStateClass.TOTAL_INCREASING,
),
"CurrentUploadRate": HuaweiSensorEntityDescription(
@@ -649,7 +622,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="current_upload_rate",
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
device_class=SensorDeviceClass.DATA_RATE,
icon="mdi:upload",
state_class=SensorStateClass.MEASUREMENT,
),
"TotalConnectTime": HuaweiSensorEntityDescription(
@@ -657,7 +629,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="total_connected_duration",
native_unit_of_measurement=UnitOfTime.SECONDS,
device_class=SensorDeviceClass.DURATION,
icon="mdi:timer-outline",
state_class=SensorStateClass.TOTAL_INCREASING,
),
"TotalDownload": HuaweiSensorEntityDescription(
@@ -665,7 +636,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="total_download",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:download",
state_class=SensorStateClass.TOTAL_INCREASING,
),
"TotalUpload": HuaweiSensorEntityDescription(
@@ -673,7 +643,6 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
translation_key="total_upload",
native_unit_of_measurement=UnitOfInformation.BYTES,
device_class=SensorDeviceClass.DATA_SIZE,
icon="mdi:upload",
state_class=SensorStateClass.TOTAL_INCREASING,
),
},
@@ -719,62 +688,50 @@ SENSOR_META: dict[str, HuaweiSensorGroup] = {
"LocalDeleted": HuaweiSensorEntityDescription(
key="LocalDeleted",
translation_key="sms_deleted_device",
icon="mdi:email-minus",
),
"LocalDraft": HuaweiSensorEntityDescription(
key="LocalDraft",
translation_key="sms_drafts_device",
icon="mdi:email-arrow-right-outline",
),
"LocalInbox": HuaweiSensorEntityDescription(
key="LocalInbox",
translation_key="sms_inbox_device",
icon="mdi:email",
),
"LocalMax": HuaweiSensorEntityDescription(
key="LocalMax",
translation_key="sms_capacity_device",
icon="mdi:email",
),
"LocalOutbox": HuaweiSensorEntityDescription(
key="LocalOutbox",
translation_key="sms_outbox_device",
icon="mdi:email-arrow-right",
),
"LocalUnread": HuaweiSensorEntityDescription(
key="LocalUnread",
translation_key="sms_unread_device",
icon="mdi:email-arrow-left",
),
"SimDraft": HuaweiSensorEntityDescription(
key="SimDraft",
translation_key="sms_drafts_sim",
icon="mdi:email-arrow-right-outline",
),
"SimInbox": HuaweiSensorEntityDescription(
key="SimInbox",
translation_key="sms_inbox_sim",
icon="mdi:email",
),
"SimMax": HuaweiSensorEntityDescription(
key="SimMax",
translation_key="sms_capacity_sim",
icon="mdi:email",
),
"SimOutbox": HuaweiSensorEntityDescription(
key="SimOutbox",
translation_key="sms_outbox_sim",
icon="mdi:email-arrow-right",
),
"SimUnread": HuaweiSensorEntityDescription(
key="SimUnread",
translation_key="sms_unread_sim",
icon="mdi:email-arrow-left",
),
"SimUsed": HuaweiSensorEntityDescription(
key="SimUsed",
translation_key="sms_messages_sim",
icon="mdi:email-arrow-left",
),
},
),
@@ -870,7 +827,7 @@ class HuaweiLteSensor(HuaweiLteBaseEntityWithDevice, SensorEntity):
"""Return icon for sensor."""
if self.entity_description.icon_fn:
return self.entity_description.icon_fn(self.state)
return self.entity_description.icon
return super().icon
@property
def device_class(self) -> SensorDeviceClass | None:
@@ -61,7 +61,7 @@
},
"data_description": {
"name": "Used to distinguish between notification services in case there are multiple Huawei LTE devices configured. Changes to this option value take effect after Home Assistant restart.",
"recipient": "Comma separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.",
"recipient": "Comma-separated list of default recipient SMS phone numbers for the notification service, used in case the notification sender does not specify any.",
"track_wired_clients": "Whether the device tracker entities track also clients attached to the router's wired Ethernet network, in addition to wireless clients.",
"unauthenticated_mode": "Whether to run in unauthenticated mode. Unauthenticated mode provides a limited set of features, but may help in case there are problems accessing the router's web interface from a browser while the integration is active. Changes to this option value take effect after integration reload."
}
@@ -12,6 +12,7 @@ from homeassistant.components.switch import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -20,7 +21,7 @@ from .const import (
KEY_DIALUP_MOBILE_DATASWITCH,
KEY_WLAN_WIFI_GUEST_NETWORK_SWITCH,
)
from .entity import HuaweiLteBaseEntityWithDevice
from .entity import HuaweiLteBaseInteractiveEntity
_LOGGER = logging.getLogger(__name__)
@@ -43,7 +44,7 @@ async def async_setup_entry(
async_add_entities(switches, True)
class HuaweiLteBaseSwitch(HuaweiLteBaseEntityWithDevice, SwitchEntity):
class HuaweiLteBaseSwitch(HuaweiLteBaseInteractiveEntity, SwitchEntity):
"""Huawei LTE switch device base class."""
key: str
@@ -57,10 +58,14 @@ class HuaweiLteBaseSwitch(HuaweiLteBaseEntityWithDevice, SwitchEntity):
def turn_on(self, **kwargs: Any) -> None:
"""Turn switch on."""
if self.router.suspended:
raise ServiceValidationError("Integration is suspended")
self._turn(state=True)
def turn_off(self, **kwargs: Any) -> None:
"""Turn switch off."""
if self.router.suspended:
raise ServiceValidationError("Integration is suspended")
self._turn(state=False)
async def async_added_to_hass(self) -> None:
@@ -3,29 +3,18 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import TYPE_CHECKING
from aioautomower.model import MowerActivities, MowerAttributes
from homeassistant.components.automation import automations_with_entity
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.components.script import scripts_with_entity
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from . import AutomowerConfigEntry
from .const import DOMAIN
from .coordinator import AutomowerDataUpdateCoordinator
from .entity import AutomowerBaseEntity
@@ -34,13 +23,6 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
def entity_used_in(hass: HomeAssistant, entity_id: str) -> list[str]:
"""Get list of related automations and scripts."""
used_in = automations_with_entity(hass, entity_id)
used_in += scripts_with_entity(hass, entity_id)
return used_in
@dataclass(frozen=True, kw_only=True)
class AutomowerBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describes Automower binary sensor entity."""
@@ -59,12 +41,6 @@ MOWER_BINARY_SENSOR_TYPES: tuple[AutomowerBinarySensorEntityDescription, ...] =
translation_key="leaving_dock",
value_fn=lambda data: data.mower.activity == MowerActivities.LEAVING,
),
AutomowerBinarySensorEntityDescription(
key="returning_to_dock",
translation_key="returning_to_dock",
value_fn=lambda data: data.mower.activity == MowerActivities.GOING_HOME,
entity_registry_enabled_default=False,
),
)
@@ -107,39 +83,3 @@ class AutomowerBinarySensorEntity(AutomowerBaseEntity, BinarySensorEntity):
def is_on(self) -> bool:
"""Return the state of the binary sensor."""
return self.entity_description.value_fn(self.mower_attributes)
async def async_added_to_hass(self) -> None:
"""Raise issue when entity is registered and was not disabled."""
if TYPE_CHECKING:
assert self.unique_id
if not (
entity_id := er.async_get(self.hass).async_get_entity_id(
BINARY_SENSOR_DOMAIN, DOMAIN, self.unique_id
)
):
return
if (
self.enabled
and self.entity_description.key == "returning_to_dock"
and entity_used_in(self.hass, entity_id)
):
async_create_issue(
self.hass,
DOMAIN,
f"deprecated_entity_{self.entity_description.key}",
breaks_in_ha_version="2025.6.0",
is_fixable=False,
severity=IssueSeverity.WARNING,
translation_key="deprecated_entity",
translation_placeholders={
"entity_name": str(self.name),
"entity": entity_id,
},
)
else:
async_delete_issue(
self.hass,
DOMAIN,
f"deprecated_task_entity_{self.entity_description.key}",
)
await super().async_added_to_hass()
@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"quality_scale": "silver",
"requirements": ["aioautomower==2025.4.4"]
"requirements": ["aioautomower==2025.5.1"]
}
@@ -67,7 +67,9 @@ rules:
reconfiguration-flow:
status: exempt
comment: no configuration possible
repair-issues: done
repair-issues:
status: exempt
comment: no issues available
stale-devices: done
# Platinum
@@ -19,10 +19,10 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
HEADLIGHT_MODES: list = [
HeadlightModes.ALWAYS_OFF.lower(),
HeadlightModes.ALWAYS_ON.lower(),
HeadlightModes.EVENING_AND_NIGHT.lower(),
HeadlightModes.EVENING_ONLY.lower(),
HeadlightModes.ALWAYS_OFF,
HeadlightModes.ALWAYS_ON,
HeadlightModes.EVENING_AND_NIGHT,
HeadlightModes.EVENING_ONLY,
]
@@ -65,13 +65,11 @@ class AutomowerSelectEntity(AutomowerControlEntity, SelectEntity):
@property
def current_option(self) -> str:
"""Return the current option for the entity."""
return cast(
HeadlightModes, self.mower_attributes.settings.headlight.mode
).lower()
return cast(HeadlightModes, self.mower_attributes.settings.headlight.mode)
@handle_sending_exception()
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
await self.coordinator.api.commands.set_headlight_mode(
self.mower_id, cast(HeadlightModes, option.upper())
self.mower_id, HeadlightModes(option)
)
@@ -39,9 +39,6 @@
"binary_sensor": {
"leaving_dock": {
"name": "Leaving dock"
},
"returning_to_dock": {
"name": "Returning to dock"
}
},
"button": {
@@ -323,12 +320,6 @@
}
}
},
"issues": {
"deprecated_entity": {
"title": "The Husqvarna Automower {entity_name} sensor is deprecated",
"description": "The Husqvarna Automower entity `{entity}` is deprecated and will be removed in a future release.\nYou can use the new returning state of the lawn mower entity instead.\nPlease update your automations and scripts to replace the sensor entity with the newly added lawn mower entity.\nWhen you are done migrating you can disable `{entity}`."
}
},
"services": {
"override_schedule": {
"name": "Override schedule",
@@ -34,6 +34,10 @@
"local_name": "ITH-21-B",
"connectable": false
},
{
"local_name": "IBS-P02B",
"connectable": false
},
{
"local_name": "Ink@IAM-T1",
"connectable": true
@@ -49,5 +53,5 @@
"dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/inkbird",
"iot_class": "local_push",
"requirements": ["inkbird-ble==0.15.0"]
"requirements": ["inkbird-ble==0.16.1"]
}
+1 -2
View File
@@ -401,8 +401,7 @@ def _categorize_programs(isy_data: IsyData, programs: Programs) -> None:
for dtype, _, node_id in folder.children:
if dtype != TAG_FOLDER:
continue
entity_folder = folder[node_id]
entity_folder: Programs = folder[node_id]
actions = None
status = entity_folder.get_by_name(KEY_STATUS)
if not status or status.protocol != PROTO_PROGRAM:
@@ -24,7 +24,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pyisy"],
"requirements": ["pyisy==3.4.0"],
"requirements": ["pyisy==3.4.1"],
"ssdp": [
{
"manufacturer": "Universal Devices Inc.",
@@ -10,7 +10,9 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN, LOGGER
@@ -62,3 +64,19 @@ class KnockiConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
data_schema=DATA_SCHEMA,
)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle a DHCP discovery."""
device_registry = dr.async_get(self.hass)
if device_entry := device_registry.async_get_device(
identifiers={(DOMAIN, discovery_info.hostname)}
):
device_registry.async_update_device(
device_entry.id,
new_connections={
(dr.CONNECTION_NETWORK_MAC, discovery_info.macaddress)
},
)
return await super().async_step_dhcp(discovery_info)
@@ -3,6 +3,11 @@
"name": "Knocki",
"codeowners": ["@joostlek", "@jgatto1", "@JakeBosh"],
"config_flow": true,
"dhcp": [
{
"hostname": "knc*"
}
],
"documentation": "https://www.home-assistant.io/integrations/knocki",
"integration_type": "hub",
"iot_class": "cloud_push",
@@ -50,10 +50,8 @@ rules:
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: This is a cloud service and does not benefit from device updates.
discovery: todo
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
+24 -10
View File
@@ -152,7 +152,10 @@ async def item_payload(item, get_thumbnail_url=None):
_LOGGER.debug("Unknown media type received: %s", media_content_type)
raise UnknownMediaType from err
thumbnail = item.get("thumbnail")
if "art" in item:
thumbnail = item["art"].get("poster", item.get("thumbnail"))
else:
thumbnail = item.get("thumbnail")
if thumbnail is not None and get_thumbnail_url is not None:
thumbnail = await get_thumbnail_url(
media_content_type, media_content_id, thumbnail_url=thumbnail
@@ -237,14 +240,16 @@ async def get_media_info(media_library, search_id, search_type):
title = None
media = None
properties = ["thumbnail"]
properties = ["thumbnail", "art"]
if search_type == MediaType.ALBUM:
if search_id:
album = await media_library.get_album_details(
album_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
album["albumdetails"].get("thumbnail")
album["albumdetails"]["art"].get(
"poster", album["albumdetails"].get("thumbnail")
)
)
title = album["albumdetails"]["label"]
media = await media_library.get_songs(
@@ -256,6 +261,7 @@ async def get_media_info(media_library, search_id, search_type):
"album",
"thumbnail",
"track",
"art",
],
)
media = media.get("songs")
@@ -274,7 +280,9 @@ async def get_media_info(media_library, search_id, search_type):
artist_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
artist["artistdetails"].get("thumbnail")
artist["artistdetails"]["art"].get(
"poster", artist["artistdetails"].get("thumbnail")
)
)
title = artist["artistdetails"]["label"]
else:
@@ -293,9 +301,10 @@ async def get_media_info(media_library, search_id, search_type):
movie_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
movie["moviedetails"].get("thumbnail")
movie["moviedetails"]["art"].get(
"poster", movie["moviedetails"].get("thumbnail")
)
)
title = movie["moviedetails"]["label"]
else:
media = await media_library.get_movies(properties)
media = media.get("movies")
@@ -305,14 +314,16 @@ async def get_media_info(media_library, search_id, search_type):
if search_id:
media = await media_library.get_seasons(
tv_show_id=int(search_id),
properties=["thumbnail", "season", "tvshowid"],
properties=["thumbnail", "season", "tvshowid", "art"],
)
media = media.get("seasons")
tvshow = await media_library.get_tv_show_details(
tv_show_id=int(search_id), properties=properties
)
thumbnail = media_library.thumbnail_url(
tvshow["tvshowdetails"].get("thumbnail")
tvshow["tvshowdetails"]["art"].get(
"poster", tvshow["tvshowdetails"].get("thumbnail")
)
)
title = tvshow["tvshowdetails"]["label"]
else:
@@ -325,7 +336,7 @@ async def get_media_info(media_library, search_id, search_type):
media = await media_library.get_episodes(
tv_show_id=int(tv_show_id),
season_id=int(season_id),
properties=["thumbnail", "tvshowid", "seasonid"],
properties=["thumbnail", "tvshowid", "seasonid", "art"],
)
media = media.get("episodes")
if media:
@@ -333,7 +344,9 @@ async def get_media_info(media_library, search_id, search_type):
season_id=int(media[0]["seasonid"]), properties=properties
)
thumbnail = media_library.thumbnail_url(
season["seasondetails"].get("thumbnail")
season["seasondetails"]["art"].get(
"poster", season["seasondetails"].get("thumbnail")
)
)
title = season["seasondetails"]["label"]
@@ -343,6 +356,7 @@ async def get_media_info(media_library, search_id, search_type):
properties=["thumbnail", "channeltype", "channel", "broadcastnow"],
)
media = media.get("channels")
title = "Channels"
return thumbnail, title, media
@@ -23,7 +23,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import issue_registry as ir
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_USE_BLUETOOTH, DOMAIN
from .coordinator import (
@@ -57,7 +57,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
assert entry.unique_id
serial = entry.unique_id
client = async_create_clientsession(hass)
client = async_get_clientsession(hass)
cloud_client = LaMarzoccoCloudClient(
username=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
@@ -70,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from ex
except RequestNotSuccessful as ex:
except (RequestNotSuccessful, TimeoutError) as ex:
_LOGGER.debug(ex, exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="api_error"
@@ -52,7 +52,7 @@ ENTITIES: tuple[LaMarzoccoBinarySensorEntityDescription, ...] = (
).status
is MachineState.BREWING
),
available_fn=lambda device: device.websocket.connected,
available_fn=lambda coordinator: not coordinator.websocket_terminated,
entity_category=EntityCategory.DIAGNOSTIC,
),
LaMarzoccoBinarySensorEntityDescription(
@@ -33,7 +33,7 @@ from homeassistant.const import (
)
from homeassistant.core import callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
@@ -83,7 +83,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
**user_input,
}
self._client = async_create_clientsession(self.hass)
self._client = async_get_clientsession(self.hass)
cloud_client = LaMarzoccoCloudClient(
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
@@ -44,6 +44,7 @@ class LaMarzoccoUpdateCoordinator(DataUpdateCoordinator[None]):
_default_update_interval = SCAN_INTERVAL
config_entry: LaMarzoccoConfigEntry
websocket_terminated = True
def __init__(
self,
@@ -92,25 +93,37 @@ class LaMarzoccoConfigUpdateCoordinator(LaMarzoccoUpdateCoordinator):
await self.device.get_dashboard()
_LOGGER.debug("Current status: %s", self.device.dashboard.to_dict())
_LOGGER.debug("Init WebSocket in background task")
self.config_entry.async_create_background_task(
hass=self.hass,
target=self.device.connect_dashboard_websocket(
update_callback=lambda _: self.async_set_updated_data(None)
),
target=self.connect_websocket(),
name="lm_websocket_task",
)
async def websocket_close(_: Any | None = None) -> None:
if self.device.websocket.connected:
await self.device.websocket.disconnect()
await self.device.websocket.disconnect()
self.config_entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, websocket_close)
)
self.config_entry.async_on_unload(websocket_close)
async def connect_websocket(self) -> None:
"""Connect to the websocket."""
_LOGGER.debug("Init WebSocket in background task")
self.websocket_terminated = False
self.async_update_listeners()
await self.device.connect_dashboard_websocket(
update_callback=lambda _: self.async_set_updated_data(None),
connect_callback=self.async_update_listeners,
disconnect_callback=self.async_update_listeners,
)
self.websocket_terminated = True
self.async_update_listeners()
class LaMarzoccoSettingsUpdateCoordinator(LaMarzoccoUpdateCoordinator):
"""Coordinator for La Marzocco settings."""
@@ -5,8 +5,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_MAC, CONF_TOKEN
from homeassistant.core import HomeAssistant
from .const import CONF_USE_BLUETOOTH
from .coordinator import LaMarzoccoConfigEntry
TO_REDACT = {
@@ -21,4 +23,12 @@ async def async_get_config_entry_diagnostics(
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data.config_coordinator
device = coordinator.device
return async_redact_data(device.to_dict(), TO_REDACT)
data = {
"device": device.to_dict(),
"bluetooth_available": {
"options_enabled": entry.options.get(CONF_USE_BLUETOOTH, True),
CONF_MAC: CONF_MAC in entry.data,
CONF_TOKEN: CONF_TOKEN in entry.data,
},
}
return async_redact_data(data, TO_REDACT)
@@ -3,7 +3,6 @@
from collections.abc import Callable
from dataclasses import dataclass
from pylamarzocco import LaMarzoccoMachine
from pylamarzocco.const import FirmwareType
from homeassistant.const import CONF_ADDRESS, CONF_MAC
@@ -23,7 +22,7 @@ from .coordinator import LaMarzoccoUpdateCoordinator
class LaMarzoccoEntityDescription(EntityDescription):
"""Description for all LM entities."""
available_fn: Callable[[LaMarzoccoMachine], bool] = lambda _: True
available_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
supported_fn: Callable[[LaMarzoccoUpdateCoordinator], bool] = lambda _: True
@@ -74,7 +73,7 @@ class LaMarzoccoEntity(LaMarzoccoBaseEntity):
def available(self) -> bool:
"""Return True if entity is available."""
if super().available:
return self.entity_description.available_fn(self.coordinator.device)
return self.entity_description.available_fn(self.coordinator)
return False
def __init__(
@@ -37,5 +37,5 @@
"iot_class": "cloud_push",
"loggers": ["pylamarzocco"],
"quality_scale": "platinum",
"requirements": ["pylamarzocco==2.0.0b6"]
"requirements": ["pylamarzocco==2.0.0b7"]
}
@@ -100,8 +100,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
.seconds.seconds_out
),
available_fn=(
lambda machine: cast(
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
lambda coordinator: cast(
PreBrewing,
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
).mode
is PreExtractionMode.PREINFUSION
),
@@ -140,8 +141,8 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
.times.pre_brewing[0]
.seconds.seconds_in
),
available_fn=lambda machine: cast(
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
available_fn=lambda coordinator: cast(
PreBrewing, coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING]
).mode
is PreExtractionMode.PREBREWING,
supported_fn=(
@@ -180,8 +181,9 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
.seconds.seconds_out
),
available_fn=(
lambda machine: cast(
PreBrewing, machine.dashboard.config[WidgetType.CM_PRE_BREWING]
lambda coordinator: cast(
PreBrewing,
coordinator.device.dashboard.config[WidgetType.CM_PRE_BREWING],
).mode
is PreExtractionMode.PREBREWING
),
+4 -1
View File
@@ -442,7 +442,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
brightness += params.pop(ATTR_BRIGHTNESS_STEP)
else:
brightness += round(params.pop(ATTR_BRIGHTNESS_STEP_PCT) / 100 * 255)
brightness_pct = round(brightness / 255 * 100)
brightness = round(
(brightness_pct + params.pop(ATTR_BRIGHTNESS_STEP_PCT)) / 100 * 255
)
params[ATTR_BRIGHTNESS] = max(0, min(255, brightness))
@@ -17,7 +17,7 @@ from .coordinator import LitterRobotDataUpdateCoordinator
_WhiskerEntityT = TypeVar("_WhiskerEntityT", bound=Robot | Pet)
def get_device_info(whisker_entity: _WhiskerEntityT) -> DeviceInfo:
def get_device_info(whisker_entity: Robot | Pet) -> DeviceInfo:
"""Get device info for a robot or pet."""
if isinstance(whisker_entity, Robot):
return DeviceInfo(
@@ -291,7 +291,7 @@
"description": "The term to search for."
},
"media_filter_classes": {
"name": "Media filter classes",
"name": "Media class filter",
"description": "List of media classes to filter the search results by."
}
}
+1 -2
View File
@@ -131,8 +131,7 @@ class MieleButton(MieleEntity, ButtonEntity):
return (
super().available
and self.entity_description.press_data
in self.coordinator.data.actions[self._device_id].process_actions
and self.entity_description.press_data in self.action.process_actions
)
async def async_press(self) -> None:
+8 -8
View File
@@ -174,6 +174,11 @@ class MieleClimate(MieleEntity, ClimateEntity):
t_key = ZONE1_DEVICES.get(
cast(MieleAppliance, self.device.device_type), "zone_1"
)
if self.device.device_type in (
MieleAppliance.FRIDGE,
MieleAppliance.FREEZER,
):
self._attr_name = None
if description.zone == 2:
if self.device.device_type in (
@@ -192,8 +197,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
if self.entity_description.target_fn(self.device) is None:
return None
return cast(float | None, self.entity_description.target_fn(self.device))
@property
@@ -201,9 +205,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
"""Return the maximum target temperature."""
return cast(
float,
self.coordinator.data.actions[self._device_id]
.target_temperature[self.entity_description.zone - 1]
.max,
self.action.target_temperature[self.entity_description.zone - 1].max,
)
@property
@@ -211,9 +213,7 @@ class MieleClimate(MieleEntity, ClimateEntity):
"""Return the minimum target temperature."""
return cast(
float,
self.coordinator.data.actions[self._device_id]
.target_temperature[self.entity_description.zone - 1]
.min,
self.action.target_temperature[self.entity_description.zone - 1].min,
)
async def async_set_temperature(self, **kwargs: Any) -> None:
+37 -11
View File
@@ -2,6 +2,8 @@
from enum import IntEnum
from pymiele import MieleEnum
DOMAIN = "miele"
MANUFACTURER = "Miele"
@@ -246,6 +248,7 @@ STATE_PROGRAM_PHASE_OVEN = {
}
STATE_PROGRAM_PHASE_WARMING_DRAWER = {
0: "not_running",
3073: "heating_up",
3075: "door_open",
3094: "keeping_warm",
3088: "cooling_down",
@@ -324,13 +327,17 @@ STATE_PROGRAM_PHASE: dict[int, dict[int, str]] = {
MieleAppliance.ROBOT_VACUUM_CLEANER: STATE_PROGRAM_PHASE_ROBOT_VACUUM_CLEANER,
}
STATE_PROGRAM_TYPE = {
0: "normal_operation_mode",
1: "own_program",
2: "automatic_program",
3: "cleaning_care_program",
4: "maintenance_program",
}
class StateProgramType(MieleEnum):
"""Defines program types."""
normal_operation_mode = 0
own_program = 1
automatic_program = 2
cleaning_care_program = 3
maintenance_program = 4
unknown = -9999
WASHING_MACHINE_PROGRAM_ID: dict[int, str] = {
-1: "no_program", # Extrapolated from other device types.
@@ -404,14 +411,21 @@ DISHWASHER_PROGRAM_ID: dict[int, str] = {
TUMBLE_DRYER_PROGRAM_ID: dict[int, str] = {
-1: "no_program", # Extrapolated from other device types.
0: "no_program", # Extrapolated from other device types
2: "cottons",
3: "minimum_iron",
4: "woollens_handcare",
5: "delicates",
6: "warm_air",
8: "express",
10: "automatic_plus",
20: "cottons",
23: "cottons_hygiene",
30: "minimum_iron",
31: "gentle_minimum_iron",
31: "bed_linen",
40: "woollens_handcare",
50: "delicates",
60: "warm_air",
66: "eco",
70: "cool_air",
80: "express",
90: "cottons",
@@ -449,17 +463,29 @@ OVEN_PROGRAM_ID: dict[int, str] = {
31: "bottom_heat",
35: "moisture_plus_auto_roast",
40: "moisture_plus_fan_plus",
48: "moisture_plus_auto_roast",
49: "moisture_plus_fan_plus",
50: "moisture_plus_intensive_bake",
51: "moisture_plus_conventional_heat",
74: "moisture_plus_intensive_bake",
76: "moisture_plus_conventional_heat",
49: "moisture_plus_fan_plus",
323: "pyrolytic",
326: "descale",
335: "shabbat_program",
336: "yom_tov",
356: "defrost",
357: "drying",
358: "heat_crockery",
360: "low_temperature_cooking",
361: "steam_cooking",
362: "keeping_warm",
512: "1_tray",
513: "2_trays",
529: "baking_tray",
554: "baiser_one_large",
555: "baiser_several_small",
556: "lemon_meringue_pie",
557: "viennese_apple_strudel",
621: "prove_15_min",
622: "prove_30_min",
623: "prove_45_min",
@@ -673,7 +699,7 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = {
2019: "defrosting_with_steam",
2020: "blanching",
2021: "bottling",
2022: "heat_crockery",
2022: "sterilize_crockery",
2023: "prove_dough",
2027: "soak",
2029: "reheating_with_microwave",
@@ -745,7 +771,7 @@ STEAM_OVEN_MICRO_PROGRAM_ID: dict[int, str] = {
2129: "potatoes_floury_diced",
2130: "german_turnip_sliced",
2131: "german_turnip_cut_into_batons",
2132: "german_turnip_sliced",
2132: "german_turnip_diced",
2133: "pumpkin_diced",
2134: "corn_on_the_cob",
2135: "mangel_cut",
+1 -1
View File
@@ -47,7 +47,7 @@ class MieleEntity(CoordinatorEntity[MieleDataUpdateCoordinator]):
return self.coordinator.data.devices[self._device_id]
@property
def actions(self) -> MieleAction:
def action(self) -> MieleAction:
"""Return the actions object."""
return self.coordinator.data.actions[self._device_id]
+4 -2
View File
@@ -99,8 +99,10 @@ class MieleFan(MieleEntity, FanEntity):
@property
def is_on(self) -> bool:
"""Return current on/off state."""
assert self.device.state_ventilation_step is not None
return self.device.state_ventilation_step > 0
return (
self.device.state_ventilation_step is not None
and self.device.state_ventilation_step > 0
)
@property
def speed_count(self) -> int:
+1 -1
View File
@@ -8,7 +8,7 @@
"iot_class": "cloud_push",
"loggers": ["pymiele"],
"quality_scale": "bronze",
"requirements": ["pymiele==0.4.1"],
"requirements": ["pymiele==0.4.3"],
"single_config_entry": true,
"zeroconf": ["_mieleathome._tcp.local."]
}
+3 -21
View File
@@ -30,9 +30,9 @@ from homeassistant.helpers.typing import StateType
from .const import (
STATE_PROGRAM_ID,
STATE_PROGRAM_PHASE,
STATE_PROGRAM_TYPE,
STATE_STATUS_TAGS,
MieleAppliance,
StateProgramType,
StateStatus,
)
from .coordinator import MieleConfigEntry, MieleDataUpdateCoordinator
@@ -181,10 +181,10 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition, ...]] = (
description=MieleSensorDescription(
key="state_program_type",
translation_key="program_type",
value_fn=lambda value: value.state_program_type,
value_fn=lambda value: StateProgramType(value.state_program_type).name,
entity_category=EntityCategory.DIAGNOSTIC,
device_class=SensorDeviceClass.ENUM,
options=sorted(set(STATE_PROGRAM_TYPE.values())),
options=sorted(set(StateProgramType.keys())),
),
),
MieleSensorDefinition(
@@ -440,8 +440,6 @@ async def async_setup_entry(
entity_class = MieleProgramIdSensor
case "state_program_phase":
entity_class = MielePhaseSensor
case "state_program_type":
entity_class = MieleTypeSensor
case _:
entity_class = MieleSensor
if (
@@ -553,22 +551,6 @@ class MielePhaseSensor(MieleSensor):
)
class MieleTypeSensor(MieleSensor):
"""Representation of the program type sensor."""
@property
def native_value(self) -> StateType:
"""Return the state of the sensor."""
ret_val = STATE_PROGRAM_TYPE.get(int(self.device.state_program_type))
if ret_val is None:
_LOGGER.debug(
"Unknown program type: %s on device type: %s",
self.device.state_program_type,
self.device.device_type,
)
return ret_val
class MieleProgramIdSensor(MieleSensor):
"""Representation of the program id sensor."""
+10 -5
View File
@@ -115,9 +115,6 @@
},
"entity": {
"binary_sensor": {
"door": {
"name": "Door"
},
"failure": {
"name": "Failure"
},
@@ -316,6 +313,8 @@
"automatic_plus": "Automatic plus",
"baking_tray": "Baking tray",
"barista_assistant": "BaristaAssistant",
"baser_one_large": "Baiser one large",
"baser_severall_small": "Baiser several small",
"basket_program": "Basket program",
"basmati_rice_rapid_steam_cooking": "Basmati rice (rapid steam cooking)",
"basmati_rice_steam_cooking": "Basmati rice (steam cooking)",
@@ -471,7 +470,7 @@
"gentle_minimum_iron": "Gentle minimum iron",
"gentle_smoothing": "Gentle smoothing",
"german_turnip_cut_into_batons": "German turnip (cut into batons)",
"german_turnip_sliced": "German turnip (sliced)",
"german_turnip_diced": "German turnip (diced)",
"gilt_head_bream_fillet": "Gilt-head bream (fillet)",
"gilt_head_bream_whole": "Gilt-head bream (whole)",
"glasses_warm": "Glasses warm",
@@ -492,7 +491,6 @@
"greenage_plums": "Greenage plums",
"halibut_fillet_2_cm": "Halibut (fillet, 2 cm)",
"halibut_fillet_3_cm": "Halibut (fillet, 3 cm)",
"heat_crockery": "Heat crockery",
"heating_damp_flannels": "Heating damp flannels",
"hens_eggs_size_l_hard": "Hens eggs (size „L“, hard)",
"hens_eggs_size_l_medium": "Hens eggs (size „L“, medium)",
@@ -532,9 +530,11 @@
"latte_macchiato": "Latte macchiato",
"leek_pieces": "Leek (pieces)",
"leek_rings": "Leek (rings)",
"lemon_meringue_pie": "Lemon meringue pie",
"long_coffee": "Long coffee",
"long_grain_rice_general_rapid_steam_cooking": "Long grain rice (general, rapid steam cooking)",
"long_grain_rice_general_steam_cooking": "Long grain rice (general, steam cooking)",
"low_temperature_cooking": "Low temperature cooking",
"maintenance": "Maintenance program",
"make_yoghurt": "Make yoghurt",
"mangel_cut": "Mangel (cut)",
@@ -673,6 +673,7 @@
"prove_dough": "Prove dough",
"pumpkin_diced": "Pumpkin (diced)",
"pumpkin_soup": "Pumpkin soup",
"pyrolytic": "Pyrolytic",
"quick_mw": "Quick MW",
"quick_power_wash": "QuickPowerWash",
"quinces_diced": "Quinces (diced)",
@@ -725,6 +726,7 @@
"sea_devil_fillet_3_cm": "Sea devil (fillet, 3 cm)",
"sea_devil_fillet_4_cm": "Sea devil (fillet, 4 cm)",
"separate_rinse_starch": "Separate rinse/starch",
"shabbat_program": "Shabbat program",
"sheyang_rapid_steam_cooking": "Sheyang (rapid steam cooking)",
"sheyang_steam_cooking": "Sheyang (steam cooking)",
"shirts": "Shirts",
@@ -755,6 +757,7 @@
"steam_care": "Steam care",
"steam_cooking": "Steam cooking",
"steam_smoothing": "Steam smoothing",
"sterilize_crockery": "Sterilize crockery",
"stuffed_cabbage": "Stuffed cabbage",
"sweat_onions": "Sweat onions",
"swede_cut_into_batons": "Swede (cut into batons)",
@@ -793,6 +796,7 @@
"veal_sausages": "Veal sausages",
"venus_clams": "Venus clams",
"very_hot_water": "Very hot water",
"viennese_apple_strudel": "Viennese apple strudel",
"viennese_silverside": "Viennese silverside",
"warm_air": "Warm air",
"wheat_cracked": "Wheat (cracked)",
@@ -817,6 +821,7 @@
"yellow_beans_cut": "Yellow beans (cut)",
"yellow_beans_whole": "Yellow beans (whole)",
"yellow_split_peas": "Yellow split peas",
"yom_tov": "Yom tov",
"zander_fillet": "Zander (fillet)"
}
},
+4 -9
View File
@@ -169,15 +169,14 @@ class MielePowerSwitch(MieleSwitch):
@property
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.coordinator.data.actions[self._device_id].power_off_enabled
return self.action.power_off_enabled
@property
def available(self) -> bool:
"""Return the availability of the entity."""
return (
self.coordinator.data.actions[self._device_id].power_off_enabled
or self.coordinator.data.actions[self._device_id].power_on_enabled
self.action.power_off_enabled or self.action.power_on_enabled
) and super().available
async def async_turn_switch(self, mode: dict[str, str | int | bool]) -> None:
@@ -192,12 +191,8 @@ class MielePowerSwitch(MieleSwitch):
"entity": self.entity_id,
},
) from err
self.coordinator.data.actions[self._device_id].power_on_enabled = cast(
bool, mode
)
self.coordinator.data.actions[self._device_id].power_off_enabled = not cast(
bool, mode
)
self.action.power_on_enabled = cast(bool, mode)
self.action.power_off_enabled = not cast(bool, mode)
self.async_write_ha_state()
+19 -19
View File
@@ -244,7 +244,6 @@
"title": "Configure MQTT device \"{mqtt_device}\"",
"description": "Please configure MQTT specific details for {platform} entity \"{entity}\":",
"data": {
"on_command_type": "ON command type",
"blue_template": "Blue template",
"brightness_template": "Brightness template",
"command_template": "Command template",
@@ -255,9 +254,10 @@
"force_update": "Force update",
"green_template": "Green template",
"last_reset_value_template": "Last reset value template",
"on_command_type": "ON command type",
"optimistic": "Optimistic",
"payload_off": "Payload off",
"payload_on": "Payload on",
"payload_off": "Payload \"off\"",
"payload_on": "Payload \"on\"",
"qos": "QoS",
"red_template": "Red template",
"retain": "Retain",
@@ -275,19 +275,19 @@
"command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to render the payload to be published at the command topic.",
"command_topic": "The publishing topic that will be used to control the {platform} entity. [Learn more.]({url}#command_topic)",
"color_temp_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract color temperature in Kelvin from the state payload value. Expected result of the template is an integer.",
"force_update": "Sends update events even if the value hasnt changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
"green_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract green color from the state payload value. Expected result of the template is an integer from 0-255 range.",
"last_reset_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the last reset. When Last reset template is set, the State class option must be Total. [Learn more.]({url}#last_reset_value_template)",
"force_update": "Sends update events even if the value hasnt changed. Useful if you want to have meaningful value graphs in history. [Learn more.]({url}#force_update)",
"on_command_type": "Defines when the `payload on` is sent. Using `last` (the default) will send any style (brightness, color, etc) topics first and then a `payload on` to the command_topic. Using `first` will send the `payload on` and then any style topics. Using `brightness` will only send brightness commands instead of the `Payload on` to turn the light on.",
"on_command_type": "Defines when the payload \"on\" is sent. Using \"Last\" (the default) will send any style (brightness, color, etc) topics first and then a payload \"on\" to the command topic. Using \"First\" will send the payload \"on\" and then any style topics. Using \"Brightness\" will only send brightness commands instead of the payload \"on\" to turn the light on.",
"optimistic": "Flag that defines if the {platform} entity works in optimistic mode. [Learn more.]({url}#optimistic)",
"payload_off": "The payload that represents the off state.",
"payload_on": "The payload that represents the on state.",
"payload_off": "The payload that represents the \"off\" state.",
"payload_on": "The payload that represents the \"on\" state.",
"qos": "The QoS value a {platform} entity should use.",
"red_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract red color from the state payload value. Expected result of the template is an integer from 0-255 range.",
"retain": "Select if values published by the {platform} entity should be retained at the MQTT broker.",
"state_template": "[Template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract state from the state payload value.",
"state_topic": "The MQTT topic subscribed to receive {platform} state values. [Learn more.]({url}#state_topic)",
"supported_color_modes": "A list of color modes supported by the list. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, WHITE. Note that if onoff or brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
"supported_color_modes": "A list of color modes supported by the light. Possible color modes are On/Off, Brightness, Color temperature, HS, XY, RGB, RGBW, RGBWW, White. Note that if On/Off or Brightness are used, that must be the only value in the list. [Learn more.]({url}#supported_color_modes)",
"value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the {platform} entity value. [Learn more.]({url}#value_template)"
},
"sections": {
@@ -325,7 +325,7 @@
"data_description": {
"brightness": "Flag that defines if light supports brightness when the RGB, RGBW, or RGBWW color mode is supported.",
"brightness_command_template": "A [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose the payload to be published at the brightness command topic.",
"brightness_command_topic": "The publishing topic that will be used to control the brigthness. [Learn more.]({url}#brightness_command_topic)",
"brightness_command_topic": "The publishing topic that will be used to control the brightness. [Learn more.]({url}#brightness_command_topic)",
"brightness_scale": "Defines the maximum brightness value (i.e., 100%) of the maximum brightness.",
"brightness_state_topic": "The MQTT topic subscribed to receive brightness state values. [Learn more.]({url}#brightness_state_topic)",
"brightness_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the brightness value."
@@ -385,7 +385,7 @@
"hs_value_template": "HS value template"
},
"data_description": {
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to hs_command_topic. Available variables: `hue` and `sat`.",
"hs_command_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-command-templates-with-mqtt) to compose message which will be sent to HS command topic. Available variables: `hue` and `sat`.",
"hs_command_topic": "The MQTT topic to publish commands to change the lights color state in HS format (Hue Saturation). Range for Hue: 0° .. 360°, Range of Saturation: 0..100. Note: Brightness is sent separately in the brightness command topic. [Learn more.]({url}#hs_command_topic)",
"hs_state_topic": "The MQTT topic subscribed to receive color state updates in HS format. The expected payload is the hue and saturation values separated by commas, for example, `359.5,100.0`. Note: Brightness is received separately in the brightness state topic. [Learn more.]({url}#hs_state_topic)",
"hs_value_template": "Defines a [template](https://www.home-assistant.io/docs/configuration/templating/#using-value-templates-with-mqtt) to extract the HS value."
@@ -574,15 +574,15 @@
"discovery": "Option to enable MQTT automatic discovery.",
"discovery_prefix": "The prefix of configuration topics the MQTT integration will subscribe to.",
"birth_enable": "When set, Home Assistant will publish an online message to your MQTT broker when MQTT is ready.",
"birth_topic": "The MQTT topic where Home Assistant will publish a `birth` message.",
"birth_payload": "The `birth` message that is published when MQTT is ready and connected.",
"birth_qos": "The quality of service of the `birth` message that is published when MQTT is ready and connected",
"birth_retain": "When set, Home Assistant will retain the `birth` message published to your MQTT broker.",
"will_enable": "When set, Home Assistant will ask your broker to publish a `will` message when MQTT is stopped or when it loses the connection to your broker.",
"will_topic": "The MQTT topic your MQTT broker will publish a `will` message to.",
"will_payload": "The message your MQTT broker `will` publish when the MQTT integration is stopped or when the connection is lost.",
"will_qos": "The quality of service of the `will` message that is published by your MQTT broker.",
"will_retain": "When set, your MQTT broker will retain the `will` message."
"birth_topic": "The MQTT topic where Home Assistant will publish a \"birth\" message.",
"birth_payload": "The \"birth\" message that is published when MQTT is ready and connected.",
"birth_qos": "The quality of service of the \"birth\" message that is published when MQTT is ready and connected",
"birth_retain": "When set, Home Assistant will retain the \"birth\" message published to your MQTT broker.",
"will_enable": "When set, Home Assistant will ask your broker to publish a \"will\" message when MQTT is stopped or when it loses the connection to your broker.",
"will_topic": "The MQTT topic your MQTT broker will publish a \"will\" message to.",
"will_payload": "The message your MQTT broker \"will\" publish when the MQTT integration is stopped or when the connection is lost.",
"will_qos": "The quality of service of the \"will\" message that is published by your MQTT broker.",
"will_retain": "When set, your MQTT broker will retain the \"will\" message."
}
}
},
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/nextbus",
"iot_class": "cloud_polling",
"loggers": ["py_nextbus"],
"requirements": ["py-nextbusnext==2.0.5"]
"requirements": ["py-nextbusnext==2.1.2"]
}
+15 -6
View File
@@ -190,7 +190,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
return_sum = 0.0
last_stats_time = None
else:
await self._async_maybe_migrate_statistics(
migrated = await self._async_maybe_migrate_statistics(
account.utility_account_id,
{
cost_statistic_id: compensation_statistic_id,
@@ -203,6 +203,13 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
return_statistic_id: return_metadata,
},
)
if migrated:
# Skip update to avoid working on old data since the migration is done
# asynchronously. Update the statistics in the next refresh in 12h.
_LOGGER.debug(
"Statistics migration completed. Skipping update for now"
)
continue
cost_reads = await self._async_get_cost_reads(
account,
self.api.utility.timezone(),
@@ -326,7 +333,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
utility_account_id: str,
migration_map: dict[str, str],
metadata_map: dict[str, StatisticMetaData],
) -> None:
) -> bool:
"""Perform one-time statistics migration based on the provided map.
Splits negative values from source IDs into target IDs.
@@ -339,7 +346,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
"""
if not migration_map:
return
return False
need_migration_source_ids = set()
for source_id, target_id in migration_map.items():
@@ -349,12 +356,12 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
1,
target_id,
True,
{},
set(),
)
if not last_target_stat:
need_migration_source_ids.add(source_id)
if not need_migration_source_ids:
return
return False
_LOGGER.info("Starting one-time migration for: %s", need_migration_source_ids)
@@ -416,7 +423,7 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
if not need_migration_source_ids:
_LOGGER.debug("No migration needed")
return
return False
for stat_id, stats in processed_stats.items():
_LOGGER.debug("Applying %d migrated stats for %s", len(stats), stat_id)
@@ -442,6 +449,8 @@ class OpowerCoordinator(DataUpdateCoordinator[dict[str, Forecast]]):
},
)
return True
async def _async_get_cost_reads(
self, account: Account, time_zone_str: str, start_time: float | None = None
) -> list[CostRead]:
@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/pushover",
"iot_class": "cloud_push",
"loggers": ["pushover_complete"],
"requirements": ["pushover_complete==1.1.1"]
"requirements": ["pushover_complete==1.2.0"]
}
@@ -98,6 +98,7 @@
"distance": "[%key:component::sensor::entity_component::distance::name%]",
"duration": "[%key:component::sensor::entity_component::duration::name%]",
"energy": "[%key:component::sensor::entity_component::energy::name%]",
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
"gas": "[%key:component::sensor::entity_component::gas::name%]",
@@ -134,6 +135,7 @@
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
"water": "[%key:component::sensor::entity_component::water::name%]",
"weight": "[%key:component::sensor::entity_component::weight::name%]",
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
}
}
@@ -13,5 +13,5 @@
"iot_class": "cloud_polling",
"loggers": ["aiokem"],
"quality_scale": "silver",
"requirements": ["aiokem==0.5.6"]
"requirements": ["aiokem==0.5.9"]
}
+7
View File
@@ -7,6 +7,7 @@ from typing import cast
from aiobotocore.client import AioBaseClient as S3Client
from aiobotocore.session import AioSession
from botocore.config import Config
from botocore.exceptions import ClientError, ConnectionError, ParamValidationError
from homeassistant.config_entries import ConfigEntry
@@ -32,6 +33,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
"""Set up S3 from a config entry."""
data = cast(dict, entry.data)
# due to https://github.com/home-assistant/core/issues/143995
config = Config(
request_checksum_calculation="when_required",
response_checksum_validation="when_required",
)
try:
session = AioSession()
# pylint: disable-next=unnecessary-dunder-call
@@ -40,6 +46,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: S3ConfigEntry) -> bool:
endpoint_url=data.get(CONF_ENDPOINT_URL),
aws_secret_access_key=data[CONF_SECRET_ACCESS_KEY],
aws_access_key_id=data[CONF_ACCESS_KEY_ID],
config=config,
).__aenter__()
await client.head_bucket(Bucket=data[CONF_BUCKET])
except ClientError as err:
+8 -2
View File
@@ -46,6 +46,7 @@ from homeassistant.const import (
CONF_TOKEN,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_component
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import format_mac
@@ -53,6 +54,7 @@ from homeassistant.util import dt as dt_util
from .const import (
CONF_SESSION_ID,
DOMAIN,
ENCRYPTED_WEBSOCKET_PORT,
LEGACY_PORT,
LOGGER,
@@ -371,9 +373,13 @@ class SamsungTVLegacyBridge(SamsungTVBridge):
except (ConnectionClosed, BrokenPipeError):
# BrokenPipe can occur when the commands is sent to fast
self._remote = None
except (UnhandledResponse, AccessDenied):
except (UnhandledResponse, AccessDenied) as err:
# We got a response so it's on.
LOGGER.debug("Failed sending command %s", key, exc_info=True)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_sending_command",
translation_placeholders={"error": repr(err), "host": self.host},
) from err
except OSError:
# Different reasons, e.g. hostname not resolveable
pass
@@ -29,13 +29,14 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.async_ import create_eager_task
from .bridge import SamsungTVWSBridge
from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, LOGGER
from .const import CONF_SSDP_RENDERING_CONTROL_LOCATION, DOMAIN, LOGGER
from .coordinator import SamsungTVConfigEntry, SamsungTVDataUpdateCoordinator
from .entity import SamsungTVEntity
@@ -308,7 +309,12 @@ class SamsungTVDevice(SamsungTVEntity, MediaPlayerEntity):
try:
await dmr_device.async_set_volume_level(volume)
except UpnpActionResponseError as err:
LOGGER.warning("Unable to set volume level on %s: %r", self._host, err)
assert self._host
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_set_volume",
translation_placeholders={"error": repr(err), "host": self._host},
) from err
async def async_volume_up(self) -> None:
"""Volume up the media player."""
@@ -68,6 +68,12 @@
"service_unsupported": {
"message": "Entity {entity} does not support this action."
},
"error_set_volume": {
"message": "Unable to set volume level on {host}: {error}"
},
"error_sending_command": {
"message": "Unable to send command to {host}: {error}"
},
"encrypted_mode_auth_failed": {
"message": "Token and session ID are required in encrypted mode."
},
@@ -42,6 +42,8 @@ from .utils import (
is_rpc_momentary_input,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class BlockBinarySensorDescription(
@@ -32,6 +32,8 @@ from .const import DOMAIN, LOGGER, SHELLY_GAS_MODELS
from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoordinator
from .utils import get_device_entry_gen, get_rpc_key_ids
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ShellyButtonDescription[
@@ -51,6 +51,8 @@ from .utils import (
is_rpc_thermostat_internal_actuator,
)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
+2
View File
@@ -21,6 +21,8 @@ from .coordinator import ShellyBlockCoordinator, ShellyConfigEntry, ShellyRpcCoo
from .entity import ShellyBlockEntity, ShellyRpcEntity
from .utils import get_device_entry_gen, get_rpc_key_ids
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
+2
View File
@@ -38,6 +38,8 @@ from .utils import (
is_rpc_momentary_input,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class ShellyBlockEventDescription(EventEntityDescription):
+2
View File
@@ -49,6 +49,8 @@ from .utils import (
percentage_to_brightness,
)
PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
@@ -42,6 +42,8 @@ from .utils import (
get_virtual_component_ids,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class BlockNumberDescription(BlockEntityDescription, NumberEntityDescription):
@@ -33,7 +33,7 @@ rules:
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: done
@@ -42,7 +42,7 @@ rules:
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
@@ -56,8 +56,8 @@ rules:
entity-device-class: done
entity-disabled-by-default: done
entity-translations: todo
exception-translations: todo
icon-translations: todo
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues: done
stale-devices:
@@ -28,6 +28,8 @@ from .utils import (
get_virtual_component_ids,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class RpcSelectDescription(RpcEntityDescription, SelectEntityDescription):
@@ -63,6 +63,8 @@ from .utils import (
is_rpc_wifi_stations_disabled,
)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class BlockSensorDescription(BlockEntityDescription, SensorEntityDescription):

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