Compare commits

...

101 Commits

Author SHA1 Message Date
Willem-Jan van Rootselaar
190ee49e3a Bump python-bsblan to version 5.1.4 (#167987) 2026-04-11 18:00:37 +00:00
Erwin Douna
f7c5a51f46 Portainer fix fetching swarm stacks (#167979) 2026-04-11 17:56:13 +00:00
tronikos
e4e9c22016 Bump opower to 0.18.1 (#167967) 2026-04-11 17:56:12 +00:00
Norbert Rittel
f2df848e3f Fix spelling of "Shut down" button label in proxmoxve (#167059) 2026-04-11 17:56:10 +00:00
Franck Nijhof
cdce98faaf Update cryptography to 46.0.7 (#167960) 2026-04-11 08:12:40 +00:00
Martin Hjelmare
fde103cdfd Fix tibber price sensor first state update (#167938)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-11 08:02:37 +00:00
Daniel Hjelseth Høyer
fcd6c6e335 Improve Tibber price coordinator (#166175)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-11 08:02:36 +00:00
Franck Nijhof
8f2cec26e3 Merge branch 'master' into rc 2026-04-10 22:45:55 +02:00
Franck Nijhof
05463cde99 Bump version to 2026.4.2 2026-04-10 20:41:40 +00:00
On Freund
a948799a6e Bump pyrisco to 0.6.8 (#167924) 2026-04-10 20:41:20 +00:00
Bram Kragten
624fab064a Update frontend to 20260325.7 (#167922) 2026-04-10 20:41:19 +00:00
Nathan Spencer
a331cb7199 Bump pylitterbot to 2025.2.1 (#167921) 2026-04-10 20:41:17 +00:00
Thomas D
7d6eaf40a6 Fix light on action for qbus integration (#167917) 2026-04-10 20:41:16 +00:00
panosmz
1ae9e7c87d Bump oasatelematics to 0.4 (#167911) 2026-04-10 20:41:14 +00:00
Thomas D
6bcfc32d48 Bump qbusmqttapi to 1.4.3 (#167909) 2026-04-10 20:41:13 +00:00
Joost Lekkerkerker
0b5f85bdb9 Bump zinvolt to 0.4.3 (#167908) 2026-04-10 20:41:12 +00:00
Maikel Punie
d153eee822 Bump velbusaio to 2026.4.0 (#167868) 2026-04-10 20:41:10 +00:00
TheJulianJES
afcc2113ce Bump ZHA to 1.1.2 (#167849) 2026-04-10 20:40:12 +00:00
J. Diego Rodríguez Royo
ae5bd63993 Fix service.yaml values for Home Connect (#167847) 2026-04-10 20:38:38 +00:00
Simone Chemelli
78107c478d Fix stale devices removal for Alexa devices (#167837) 2026-04-10 20:38:36 +00:00
Joost Lekkerkerker
84490ef0bb Support Chess.com accounts with no name (#167824) 2026-04-10 20:38:34 +00:00
Raj Laud
887e14638b Fix Victron BLE storage errors caused by non-serializable value_fn callable in sensor entity description (#167819)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 20:38:32 +00:00
Raj Laud
818bde1d5e Fix Victron BLE false reauth triggered by unknown enum bitmask combinations (#167809)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 20:38:31 +00:00
Benjamin Hudgens
83da18b761 Revert "Fix Ring snapshots" - #164337 (#167790) 2026-04-10 20:38:29 +00:00
Maciej Bieniek
bd904caea1 Bump aiotractive to 1.0.2 (#167783) 2026-04-10 20:38:28 +00:00
Michael
500f030eaa Set proper state for the internet_access switches in FRITZ!Box Tools (#167767) 2026-04-10 20:38:26 +00:00
wollew
ce755f5f8f Bump pyvlx to 0.2.33 (#167764) 2026-04-10 20:38:25 +00:00
Nick Haghiri
fb766d164b Improve error logging for Backblaze B2 upload failures (#167721) 2026-04-10 20:38:23 +00:00
Tom
394670e33f Fix ProxmoxVE migration causing reauthentication (#167624) 2026-04-10 20:38:21 +00:00
G Johansson
f79285f9ab Bump holidays to 0.94 (#167604) 2026-04-10 20:38:20 +00:00
Erik Montnemery
a422611ada Fix securetar size calculation when encrypting backup (#167602) 2026-04-10 20:38:18 +00:00
Erik Montnemery
4c34dcd560 Bump securetar to 2026.4.0 (#167600) 2026-04-10 20:38:17 +00:00
Maciej Bieniek
1aca993c12 Fix Tractive switch availability (#167599) 2026-04-10 20:38:15 +00:00
Leo Periou
a8cc099b66 fix EWS deviceType problem (#167597) 2026-04-10 20:38:13 +00:00
Artur Pragacz
c56d67c02f Set up condition and trigger helpers in check config script (#167589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 20:38:11 +00:00
Jan Čermák
0ce98cfb34 Remove homeassistant/actions/helpers/info from builder workflow (#167573) 2026-04-10 20:35:03 +00:00
Jan Bouwhuis
4a13ab9aff Bump incomfort-client to v0.7.0 (#167546) 2026-04-10 20:28:23 +00:00
Fabian Neundorf
dc65646d8b Bump python-picnic-api2 to 1.3.4 (#167539)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-10 20:28:22 +00:00
Nelson Osacky
39fbdad775 Add missing Miele dishwasher program ID 201 (#167536)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:28:20 +00:00
Jordan Harvey
b4f6a43a14 Bump pynintendoparental to 2.3.4 (#167510) 2026-04-10 20:28:19 +00:00
Nick Haghiri
e5ff7a9944 Handle BadRequest exception in Backblaze B2 config flow and setup (#167482) 2026-04-10 20:28:18 +00:00
Nick Haghiri
ca9945f750 Bump b2sdk to 2.10.4 (#167481) 2026-04-10 20:28:16 +00:00
Andrea Turri
b028e2a6ae Miele - fix core temperature reading (#167476) 2026-04-10 20:28:15 +00:00
Allen Porter
6f4aca495b Update roborock services to raise ServiceNotSupported for new devices that don't yet support it (#167470)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 20:28:14 +00:00
Jamie Magee
a892b5364d Fix nzbget positional argument mismatch in NZBGetAPI calls (#167456) 2026-04-10 20:28:12 +00:00
Steve Easley
f57e682a98 Bump jvcprojector dependency to pyjvcprojector 2.0.5 (#167450) 2026-04-10 20:28:11 +00:00
Nils Ove Erstad
3493517b6d Fix missing color_mode initialization in MQTT JSON light schema (#167429) 2026-04-10 20:28:09 +00:00
Jordan Harvey
b5842b8484 Fix handling of missing period statistics in Anglian Water coordinator (#167427)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 20:28:08 +00:00
Willem-Jan van Rootselaar
3333b8d019 Fix setup without dhw (#167423) 2026-04-10 20:28:07 +00:00
Simone Chemelli
745860553c Bump aiocomelit to 2.0.2 (#167414) 2026-04-10 20:26:02 +00:00
Shai Ungar
7188a09a59 Use dedicated session for seventeentrack to preserve login cookies (#167394)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:26:01 +00:00
Robert Svensson
96a9b89412 Bump axis to v68 to improve MQTT event resiliance (#167373) 2026-04-10 20:25:59 +00:00
Tom
586d7ab526 Improve ProxmoxVE permissions handling (#167370) 2026-04-10 20:25:58 +00:00
Joost Lekkerkerker
5f2fe4ffd4 Bump aiohue to 4.8.1 (#167369) 2026-04-10 20:25:56 +00:00
Simone Chemelli
040192c103 Align and cleanup tests data for Fritz (#167363) 2026-04-10 20:25:55 +00:00
Jordan Harvey
e85430105e Bump cryptography to 46.0.6 (#167330)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-04-10 20:22:31 +00:00
cdheiser
5c7c0a6e83 Bump pylutron to 0.4.1 (#167324) 2026-04-10 20:21:53 +00:00
007hacky007
c7bd673d01 Bump afsapi to 0.3.1 (#167321) 2026-04-10 20:21:51 +00:00
MarkGodwin
6d3a93df81 Update to tplink-omada-client 1.5.7 (#167313) 2026-04-10 20:21:50 +00:00
Raj Laud
6a934b5fe3 Fix victron ble reauth flow title (#167307)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-10 20:21:48 +00:00
Daniel Hjelseth Høyer
d644348dc8 Bump pyTibber to 0.37.0 (#167283) 2026-04-10 20:21:47 +00:00
Ludovic BOUÉ
dbfde9266c Add Hisense AC (0x138C/0x0101) to Matter dry and fan mode device lists (#167282)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-10 20:21:45 +00:00
Simone Chemelli
ed0b68ec4a Allow force alarm actions for Comelit (#167202) 2026-04-10 20:21:44 +00:00
Patrick
c32d523f63 Bump starlink-grpc-core to 1.2.5 (#167195) 2026-04-10 20:21:42 +00:00
Marco Sousa
98a4e27e35 Bump aiopvpc to 4.3.1 (#167189) 2026-04-10 20:21:41 +00:00
Alex Merkel
fb1365e9a4 [LG Soundbar] Fix incorrect state for some models (#167094) 2026-04-10 20:21:39 +00:00
Willem-Jan van Rootselaar
850b034a5f Include port in BSB-LAN configuration URL when non-default (#166480) 2026-04-10 20:19:09 +00:00
Samuel Xiao
b880876e0e Switchbot Cloud: Enable Webhook for Bot (#165647) 2026-04-10 20:13:56 +00:00
Jeef
ab601e5717 Prevent the intellifire client from polling independently of its coordinator (#165341)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-10 20:13:54 +00:00
Matt Philips
7eda592c72 Improve handling of disconnected meters with Rainforest Automation Eagle-200 (#161185)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-10 20:13:53 +00:00
Franck Nijhof
b981ece163 Pin actions/helpers/info to fix release build (#167327) 2026-04-03 20:58:54 +00:00
Franck Nijhof
7ea931fdc8 2026.4.1 (#167310) 2026-04-03 22:10:54 +02:00
Franck Nijhof
f3038a20af Bump version to 2026.4.1 2026-04-03 16:05:08 +00:00
Pete Sage
de234c7190 Sonos alarm switch entities may not be created when speaker offline initially (#167303) 2026-04-03 16:01:17 +00:00
Pete Sage
399681984f Bump soco to 0.30.15 (#167299) 2026-04-03 16:01:16 +00:00
Joost Lekkerkerker
5ca14ca7d7 Bump Zinvolt to 0.4.1 (#167296) 2026-04-03 16:01:15 +00:00
Joost Lekkerkerker
ac53cfa85a Make sure we take all Zinvolt battery units in account (#167294) 2026-04-03 16:01:13 +00:00
Ludovic BOUÉ
02f1a9c3a9 Fix Matter water heater off mode (#167286)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 16:01:12 +00:00
Bram Kragten
f93fdceac9 Update frontend to 20260325.6 (#167285) 2026-04-03 16:01:11 +00:00
Ludovic BOUÉ
711a89f7b8 Fix to allow Matter Fan percent setting to be null when FanMode is Auto (#167279)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-03 16:01:09 +00:00
Norbert Rittel
19e58c554e Improve Assist satellite action naming consistency (#167278) 2026-04-03 16:01:08 +00:00
Joost Lekkerkerker
feb6c2bfe6 Bump zinvolt to 0.4.0 (#167276) 2026-04-03 16:01:07 +00:00
Norbert Rittel
6bb91422ff Improve Media player action naming consistency (#167274)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-03 16:01:06 +00:00
Andrew Jackson
3bd699285b Remove Transmission port forward sensor (#167269) 2026-04-03 16:00:17 +00:00
dotlambda
6d10305197 Bump psutil to 7.2.2 (#167263) 2026-04-03 15:57:57 +00:00
Joakim Plate
42a9c8488d Update arcam to 1.8.3 (#167249) 2026-04-03 15:57:56 +00:00
Norbert Rittel
c6c273559e Improve Recorder action naming consistency (#167244) 2026-04-03 15:57:55 +00:00
Pete Sage
f7394ce302 Fix Sonos reporting wrong state when media title is whitespace (#167223) 2026-04-03 15:57:53 +00:00
G Johansson
175dec6f1a Bump holiday library to 0.93 (#167217) 2026-04-03 15:57:52 +00:00
G Johansson
d137761cb5 Fix SMHI (#167212) 2026-04-03 15:57:50 +00:00
Simone Chemelli
8055cbc58d Migrate image unique_id for Fritz (#167209) 2026-04-03 15:57:49 +00:00
Joost Lekkerkerker
c9dff27590 Remove not implemented supported feature from Wiim (#167205) 2026-04-03 15:57:48 +00:00
Mike Degatano
c913a858b6 Wrap hassio import in is_hassio check in get_system_info helper (#167111) 2026-04-03 15:57:46 +00:00
Joost Lekkerkerker
4ed33a804e Bump pySmartThings to 3.7.3 (#167075) 2026-04-03 15:57:45 +00:00
Kevin O'Brien
8bf5674826 Fix Proxmox VE backup status sensor false positive due to case mismatch (#167069)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-03 15:57:43 +00:00
Manu
b8a0b0083b Fix websocket calling async_release_notes in update component although unavailable (#167067) 2026-04-03 15:57:42 +00:00
Bram Kragten
a57c101b5e Fix select condition state selector (#167064) 2026-04-03 15:57:41 +00:00
Brett Adams
957b8c1c52 Fix Tesla Fleet OAuth scope refresh during reauth (#166920) 2026-04-03 15:57:40 +00:00
Brett Adams
bb002d051b Fix Tesla Fleet charge current scope handling (#166919) 2026-04-03 15:57:38 +00:00
LTek
2b2fd4ac92 Fix Ring snapshots (#164337)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-03 15:57:37 +00:00
Jan Bouwhuis
f4c270629b Fix tuya energy sensor units (#160392) 2026-04-03 15:57:35 +00:00
197 changed files with 8992 additions and 12186 deletions

View File

@@ -49,7 +49,7 @@ jobs:
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@master # zizmor: ignore[unpinned-uses]
uses: home-assistant/actions/helpers/info@5f5b077d63a1e4c53019231409a0c4d791fb74e5 # zizmor: ignore[unpinned-uses]
- name: Get version
id: version

View File

@@ -54,7 +54,16 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
entry.data[CONF_PASSWORD],
entry.data[CONF_LOGIN_DATA],
)
self.previous_devices: set[str] = set()
device_registry = dr.async_get(hass)
self.previous_devices: set[str] = {
identifier
for device in device_registry.devices.get_devices_for_config_entry_id(
entry.entry_id
)
if device.entry_type != dr.DeviceEntryType.SERVICE
for identifier_domain, identifier in device.identifiers
if identifier_domain == DOMAIN
}
async def _async_update_data(self) -> dict[str, AmazonDevice]:
"""Update device data."""

View File

@@ -92,6 +92,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
_LOGGER.debug("Updating statistics for the first time")
usage_sum = 0.0
last_stats_time = None
allow_update_last_stored_hour = False
else:
if not meter.readings or len(meter.readings) == 0:
_LOGGER.debug("No recent usage statistics found, skipping update")
@@ -107,6 +108,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
_LOGGER.debug("Getting statistics at %s", start)
stats: dict[str, list[Any]] = {}
for end in (start + timedelta(seconds=1), None):
stats = await get_instance(self.hass).async_add_executor_job(
statistics_during_period,
@@ -127,15 +129,28 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
"Not found, trying to find oldest statistic after %s",
start,
)
assert stats
def _safe_get_sum(records: list[Any]) -> float:
if records and "sum" in records[0]:
return float(records[0]["sum"])
return 0.0
if not stats or not stats.get(usage_statistic_id):
_LOGGER.debug(
"Could not find existing statistics during period lookup for %s, "
"falling back to last stored statistic",
usage_statistic_id,
)
allow_update_last_stored_hour = True
last_records = last_stat[usage_statistic_id]
usage_sum = float(last_records[0].get("sum") or 0.0)
last_stats_time = last_records[0]["start"]
else:
allow_update_last_stored_hour = False
records = stats[usage_statistic_id]
usage_sum = _safe_get_sum(stats.get(usage_statistic_id, []))
last_stats_time = stats[usage_statistic_id][0]["start"]
def _safe_get_sum(records: list[Any]) -> float:
if records and "sum" in records[0]:
return float(records[0]["sum"])
return 0.0
usage_sum = _safe_get_sum(records)
last_stats_time = records[0]["start"]
usage_statistics = []
@@ -148,7 +163,13 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
)
continue
start = dt_util.as_local(parsed_read_at) - timedelta(hours=1)
if last_stats_time is not None and start.timestamp() <= last_stats_time:
if last_stats_time is not None and (
start.timestamp() < last_stats_time
or (
start.timestamp() == last_stats_time
and not allow_update_last_stored_hour
)
):
continue
usage_state = max(0, read["consumption"] / 1000)
usage_sum = max(0, read["read"])

View File

@@ -7,7 +7,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.2"],
"requirements": ["arcam-fmj==1.8.3"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@@ -91,6 +91,7 @@ SENSORS: tuple[ArcamFmjSensorEntityDescription, ...] = (
value_fn=lambda state: (
vp.colorspace.name.lower()
if (vp := state.get_incoming_video_parameters()) is not None
and vp.colorspace is not None
else None
),
),

View File

@@ -75,7 +75,7 @@
},
"services": {
"announce": {
"description": "Lets a satellite announce a message.",
"description": "Lets an Assist satellite announce a message.",
"fields": {
"media_id": {
"description": "The media ID to announce instead of using text-to-speech.",
@@ -94,10 +94,10 @@
"name": "Preannounce media ID"
}
},
"name": "Announce"
"name": "Announce on satellite"
},
"ask_question": {
"description": "Asks a question and gets the user's response.",
"description": "Lets an Assist satellite ask a question and get the user's response.",
"fields": {
"answers": {
"description": "Possible answers to the question.",
@@ -124,10 +124,10 @@
"name": "Question media ID"
}
},
"name": "Ask question"
"name": "Ask question on satellite"
},
"start_conversation": {
"description": "Starts a conversation from a satellite.",
"description": "Starts a conversation from an Assist satellite.",
"fields": {
"extra_system_prompt": {
"description": "Provide background information to the AI about the request.",
@@ -150,13 +150,13 @@
"name": "Message"
}
},
"name": "Start conversation"
"name": "Start conversation on satellite"
}
},
"title": "Assist satellite",
"triggers": {
"idle": {
"description": "Triggers after one or more voice assistant satellites become idle after having processed a command.",
"description": "Triggers after one or more Assist satellites become idle after having processed a command.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -165,7 +165,7 @@
"name": "Satellite became idle"
},
"listening": {
"description": "Triggers after one or more voice assistant satellites start listening for a command from someone.",
"description": "Triggers after one or more Assist satellites start listening for a command from someone.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -174,7 +174,7 @@
"name": "Satellite started listening"
},
"processing": {
"description": "Triggers after one or more voice assistant satellites start processing a command after having heard it.",
"description": "Triggers after one or more Assist satellites start processing a command after having heard it.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"
@@ -183,7 +183,7 @@
"name": "Satellite started processing"
},
"responding": {
"description": "Triggers after one or more voice assistant satellites start responding to a command after having processed it, or start announcing something.",
"description": "Triggers after one or more Assist satellites start responding to a command after having processed it, or start announcing something.",
"fields": {
"behavior": {
"name": "[%key:component::assist_satellite::common::trigger_behavior_name%]"

View File

@@ -29,7 +29,7 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["axis"],
"requirements": ["axis==67"],
"requirements": ["axis==68"],
"ssdp": [
{
"manufacturer": "AXIS"

View File

@@ -74,6 +74,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
translation_domain=DOMAIN,
translation_key="invalid_bucket_name",
) from err
except exception.BadRequest as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="bad_request",
translation_placeholders={"error_message": str(err)},
) from err
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,

View File

@@ -101,8 +101,7 @@ def handle_b2_errors[T](
try:
return await func(*args, **kwargs)
except B2Error as err:
error_msg = f"Failed during {func.__name__}"
raise BackupAgentError(error_msg) from err
raise BackupAgentError(f"Failed during {func.__name__}: {err}") from err
return wrapper
@@ -170,8 +169,7 @@ class BackblazeBackupAgent(BackupAgent):
async def _cleanup_failed_upload(self, filename: str) -> None:
"""Clean up a partially uploaded file after upload failure."""
_LOGGER.warning(
"Attempting to delete partially uploaded main backup file %s "
"due to metadata upload failure",
"Attempting to delete partially uploaded backup file %s",
filename,
)
try:
@@ -180,11 +178,10 @@ class BackblazeBackupAgent(BackupAgent):
)
await self._hass.async_add_executor_job(uploaded_main_file_info.delete)
except B2Error:
_LOGGER.debug(
"Failed to clean up partially uploaded main backup file %s. "
"Manual intervention may be required to delete it from Backblaze B2",
_LOGGER.warning(
"Failed to clean up partially uploaded backup file %s;"
" manual deletion from Backblaze B2 may be required",
filename,
exc_info=True,
)
else:
_LOGGER.debug(
@@ -256,9 +253,10 @@ class BackblazeBackupAgent(BackupAgent):
prefixed_metadata_filename,
)
upload_successful = False
tar_uploaded = False
try:
await self._upload_backup_file(prefixed_tar_filename, open_stream, {})
tar_uploaded = True
_LOGGER.debug(
"Main backup file upload finished for %s", prefixed_tar_filename
)
@@ -270,15 +268,14 @@ class BackblazeBackupAgent(BackupAgent):
_LOGGER.debug(
"Metadata file upload finished for %s", prefixed_metadata_filename
)
upload_successful = True
finally:
if upload_successful:
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
self._invalidate_caches(
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
)
else:
_LOGGER.debug("Backup upload complete: %s", prefixed_tar_filename)
self._invalidate_caches(
backup.backup_id, prefixed_tar_filename, prefixed_metadata_filename
)
except B2Error:
if tar_uploaded:
await self._cleanup_failed_upload(prefixed_tar_filename)
raise
def _upload_metadata_file_sync(
self, metadata_content: bytes, filename: str

View File

@@ -174,6 +174,14 @@ class BackblazeConfigFlow(ConfigFlow, domain=DOMAIN):
"Backblaze B2 bucket '%s' does not exist", user_input[CONF_BUCKET]
)
errors[CONF_BUCKET] = "invalid_bucket_name"
except exception.BadRequest as err:
_LOGGER.error(
"Backblaze B2 API rejected the request for Key ID '%s': %s",
user_input[CONF_KEY_ID],
err,
)
errors["base"] = "bad_request"
placeholders["error_message"] = str(err)
except (
exception.B2ConnectionError,
exception.B2RequestTimeout,

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["b2sdk"],
"quality_scale": "bronze",
"requirements": ["b2sdk==2.10.1"]
"requirements": ["b2sdk==2.10.4"]
}

View File

@@ -6,6 +6,7 @@
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"bad_request": "The Backblaze B2 API rejected the request: {error_message}",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_bucket_name": "[%key:component::backblaze_b2::exceptions::invalid_bucket_name::message%]",
"invalid_capability": "[%key:component::backblaze_b2::exceptions::invalid_capability::message%]",
@@ -60,6 +61,9 @@
}
},
"exceptions": {
"bad_request": {
"message": "The Backblaze B2 API rejected the request: {error_message}"
},
"cannot_connect": {
"message": "Cannot connect to endpoint"
},

View File

@@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "calculated",
"quality_scale": "internal",
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
"requirements": ["cronsim==2.7", "securetar==2026.4.0"],
"single_config_entry": true
}

View File

@@ -22,6 +22,7 @@ from securetar import (
SecureTarFile,
SecureTarReadError,
SecureTarRootKeyContext,
get_archive_max_ciphertext_size,
)
from homeassistant.core import HomeAssistant
@@ -431,7 +432,9 @@ class _CipherBackupStreamer:
def size(self) -> int:
"""Return the maximum size of the decrypted or encrypted backup."""
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
return get_archive_max_ciphertext_size( # type: ignore[no-any-return]
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
)
def _num_tar_files(self) -> int:
"""Return the number of inner tar files."""

View File

@@ -10,6 +10,7 @@ from bsblan import (
BSBLAN,
BSBLANAuthError,
BSBLANConnectionError,
BSBLANError,
HotWaterConfig,
HotWaterSchedule,
HotWaterState,
@@ -50,7 +51,7 @@ class BSBLanFastData:
state: State
sensor: Sensor
dhw: HotWaterState
dhw: HotWaterState | None = None
@dataclass
@@ -111,7 +112,6 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
# This reduces response time significantly (~0.2s per parameter)
state = await self.client.state(include=STATE_INCLUDE)
sensor = await self.client.sensor(include=SENSOR_INCLUDE)
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
except BSBLANAuthError as err:
raise ConfigEntryAuthFailed(
@@ -126,6 +126,19 @@ class BSBLanFastCoordinator(BSBLanCoordinator[BSBLanFastData]):
translation_placeholders={"host": host},
) from err
# Fetch DHW state separately - device may not support hot water
dhw: HotWaterState | None = None
try:
dhw = await self.client.hot_water_state(include=DHW_STATE_INCLUDE)
except BSBLANError:
# Preserve last known DHW state if available (entity may depend on it)
if self.data:
dhw = self.data.dhw
LOGGER.debug(
"DHW (Domestic Hot Water) state not available on device at %s",
self.config_entry.data[CONF_HOST],
)
return BSBLanFastData(
state=state,
sensor=sensor,
@@ -159,13 +172,6 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
dhw_config = await self.client.hot_water_config(include=DHW_CONFIG_INCLUDE)
dhw_schedule = await self.client.hot_water_schedule()
except AttributeError:
# Device does not support DHW functionality
LOGGER.debug(
"DHW (Domestic Hot Water) not available on device at %s",
self.config_entry.data[CONF_HOST],
)
return BSBLanSlowData()
except (BSBLANConnectionError, BSBLANAuthError) as err:
# If config update fails, keep existing data
LOGGER.debug(
@@ -177,6 +183,13 @@ class BSBLanSlowCoordinator(BSBLanCoordinator[BSBLanSlowData]):
return self.data
# First fetch failed, return empty data
return BSBLanSlowData()
except BSBLANError, AttributeError:
# Device does not support DHW functionality
LOGGER.debug(
"DHW (Domestic Hot Water) not available on device at %s",
self.config_entry.data[CONF_HOST],
)
return BSBLanSlowData()
return BSBLanSlowData(
dhw_config=dhw_config,

View File

@@ -22,7 +22,9 @@ async def async_get_config_entry_diagnostics(
"fast_coordinator_data": {
"state": data.fast_coordinator.data.state.model_dump(),
"sensor": data.fast_coordinator.data.sensor.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump()
if data.fast_coordinator.data.dhw
else None,
},
"static": data.static.model_dump() if data.static is not None else None,
}

View File

@@ -2,6 +2,9 @@
from __future__ import annotations
from yarl import URL
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -10,7 +13,7 @@ from homeassistant.helpers.device_registry import (
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BSBLanData
from .const import DOMAIN
from .const import DEFAULT_PORT, DOMAIN
from .coordinator import BSBLanCoordinator, BSBLanFastCoordinator, BSBLanSlowCoordinator
@@ -22,7 +25,8 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
def __init__(self, coordinator: _T, data: BSBLanData) -> None:
"""Initialize BSBLan entity with device info."""
super().__init__(coordinator)
host = coordinator.config_entry.data["host"]
host = coordinator.config_entry.data[CONF_HOST]
port = coordinator.config_entry.data.get(CONF_PORT, DEFAULT_PORT)
mac = data.device.MAC
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, mac)},
@@ -44,7 +48,7 @@ class BSBLanEntityBase[_T: BSBLanCoordinator](CoordinatorEntity[_T]):
else None
),
sw_version=data.device.version,
configuration_url=f"http://{host}",
configuration_url=str(URL.build(scheme="http", host=host, port=port)),
)

View File

@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==5.1.3"],
"requirements": ["python-bsblan==5.1.4"],
"zeroconf": [
{
"name": "bsb-lan*",

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any
from bsblan import BSBLANError, SetHotWaterParam
from bsblan import BSBLANError, HotWaterState, SetHotWaterParam
from homeassistant.components.water_heater import (
STATE_ECO,
@@ -46,8 +46,10 @@ async def async_setup_entry(
data = entry.runtime_data
# Only create water heater entity if DHW (Domestic Hot Water) is available
# Check if we have any DHW-related data indicating water heater support
dhw_data = data.fast_coordinator.data.dhw
if dhw_data is None:
# Device does not support DHW, skip water heater setup
return
if (
dhw_data.operating_mode is None
and dhw_data.nominal_setpoint is None
@@ -107,11 +109,21 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
else:
self._attr_max_temp = 65.0 # Default maximum
@property
def _dhw(self) -> HotWaterState:
"""Return DHW state data.
This entity is only created when DHW data is available.
"""
dhw = self.coordinator.data.dhw
assert dhw is not None
return dhw
@property
def current_operation(self) -> str | None:
"""Return current operation."""
if (
operating_mode := self.coordinator.data.dhw.operating_mode
operating_mode := self._dhw.operating_mode
) is None or operating_mode.value is None:
return None
return BSBLAN_TO_HA_OPERATION_MODE.get(operating_mode.value)
@@ -119,16 +131,14 @@ class BSBLANWaterHeater(BSBLanDualCoordinatorEntity, WaterHeaterEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if (
current_temp := self.coordinator.data.dhw.dhw_actual_value_top_temperature
) is None:
if (current_temp := self._dhw.dhw_actual_value_top_temperature) is None:
return None
return current_temp.value
@property
def target_temperature(self) -> float | None:
"""Return the temperature we try to reach."""
if (target_temp := self.coordinator.data.dhw.nominal_setpoint) is None:
if (target_temp := self._dhw.nominal_setpoint) is None:
return None
return target_temp.value

View File

@@ -39,7 +39,9 @@ class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
else:
await self.async_set_unique_id(str(user.player_id))
self._abort_if_unique_id_configured()
return self.async_create_entry(title=user.name, data=user_input)
return self.async_create_entry(
title=user.name or user.username, data=user_input
)
return self.async_show_form(
step_id="user",

View File

@@ -112,7 +112,7 @@ class ComelitAlarmEntity(
@property
def available(self) -> bool:
"""Return True if alarm is available."""
if self._area.human_status in [AlarmAreaState.ANOMALY, AlarmAreaState.UNKNOWN]:
if self._area.human_status == AlarmAreaState.UNKNOWN:
return False
return super().available
@@ -151,7 +151,7 @@ class ComelitAlarmEntity(
if code != str(self.coordinator.api.device_pin):
return
await self.coordinator.api.set_zone_status(
self._area.index, ALARM_ACTIONS[DISABLE]
self._area.index, ALARM_ACTIONS[DISABLE], self._area.anomaly
)
await self._async_update_state(
AlarmAreaState.DISARMED, ALARM_AREA_ARMED_STATUS[DISABLE]
@@ -160,7 +160,7 @@ class ComelitAlarmEntity(
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
await self.coordinator.api.set_zone_status(
self._area.index, ALARM_ACTIONS[AWAY]
self._area.index, ALARM_ACTIONS[AWAY], self._area.anomaly
)
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[AWAY]
@@ -169,7 +169,7 @@ class ComelitAlarmEntity(
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
await self.coordinator.api.set_zone_status(
self._area.index, ALARM_ACTIONS[HOME]
self._area.index, ALARM_ACTIONS[HOME], self._area.anomaly
)
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[HOME_P1]
@@ -178,7 +178,7 @@ class ComelitAlarmEntity(
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
await self.coordinator.api.set_zone_status(
self._area.index, ALARM_ACTIONS[NIGHT]
self._area.index, ALARM_ACTIONS[NIGHT], self._area.anomaly
)
await self._async_update_state(
AlarmAreaState.ARMED, ALARM_AREA_ARMED_STATUS[NIGHT]

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiocomelit"],
"quality_scale": "platinum",
"requirements": ["aiocomelit==2.0.1"]
"requirements": ["aiocomelit==2.0.2"]
}

View File

@@ -448,10 +448,13 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
if not attributes.get("MACAddress"):
continue
wan_access_result = None
if (wan_access := attributes.get("X_AVM-DE_WANAccess")) is not None:
wan_access_result = "granted" in wan_access
else:
wan_access_result = None
# wan_access can be "granted", "denied", "unknown" or "error"
if "granted" in wan_access:
wan_access_result = True
elif "denied" in wan_access:
wan_access_result = False
hosts[attributes["MACAddress"]] = Device(
name=attributes["HostName"],

View File

@@ -10,9 +10,11 @@ from requests.exceptions import RequestException
from homeassistant.components.image import ImageEntity
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util, slugify
from .const import DOMAIN, Platform
from .coordinator import AvmWrapper, FritzConfigEntry
from .entity import FritzBoxBaseEntity
@@ -22,6 +24,32 @@ _LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
async def _migrate_to_new_unique_id(
hass: HomeAssistant, avm_wrapper: AvmWrapper, ssid: str
) -> None:
"""Migrate old unique id to new unique id."""
old_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
new_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
entity_registry = er.async_get(hass)
entity_id = entity_registry.async_get_entity_id(
Platform.IMAGE,
DOMAIN,
old_unique_id,
)
if entity_id is None:
return
entity_registry.async_update_entity(entity_id, new_unique_id=new_unique_id)
_LOGGER.debug(
"Migrating guest Wi-Fi image unique_id from [%s] to [%s]",
old_unique_id,
new_unique_id,
)
async def async_setup_entry(
hass: HomeAssistant,
entry: FritzConfigEntry,
@@ -34,6 +62,8 @@ async def async_setup_entry(
avm_wrapper.fritz_guest_wifi.get_info
)
await _migrate_to_new_unique_id(hass, avm_wrapper, guest_wifi_info["NewSSID"])
async_add_entities(
[
FritzGuestWifiQRImage(
@@ -60,7 +90,7 @@ class FritzGuestWifiQRImage(FritzBoxBaseEntity, ImageEntity):
) -> None:
"""Initialize the image entity."""
self._attr_name = ssid
self._attr_unique_id = slugify(f"{avm_wrapper.unique_id}-{ssid}-qr-code")
self._attr_unique_id = f"{avm_wrapper.unique_id}-guest_wifi_qr_code"
self._current_qr_bytes: bytes | None = None
super().__init__(avm_wrapper, device_friendly_name)
ImageEntity.__init__(self, hass)

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260325.5"]
"requirements": ["home-assistant-frontend==20260325.7"]
}

View File

@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/frontier_silicon",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["afsapi==0.2.7"],
"requirements": ["afsapi==0.3.1"],
"ssdp": [
{
"st": "urn:schemas-frontier-silicon-com:undok:fsapi:1"

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.84", "babel==2.15.0"]
"requirements": ["holidays==0.94", "babel==2.15.0"]
}

View File

@@ -68,8 +68,16 @@ PROGRAM_OPTIONS = {
),
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: vol.All(int, vol.Range(min=0)),
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
OptionKey.LAUNDRY_CARE_COMMON_SILENT_MODE: bool,
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
OptionKey.LAUNDRY_CARE_WASHER_INTENSIVE_PLUS: bool,
OptionKey.LAUNDRY_CARE_WASHER_LESS_IRONING: bool,
OptionKey.LAUNDRY_CARE_WASHER_MINI_LOAD: bool,
OptionKey.LAUNDRY_CARE_WASHER_PREWASH: bool,
OptionKey.LAUNDRY_CARE_WASHER_RINSE_HOLD: bool,
OptionKey.LAUNDRY_CARE_WASHER_SOAK: bool,
OptionKey.LAUNDRY_CARE_WASHER_WATER_PLUS: bool,
}.items()
}

View File

@@ -119,7 +119,7 @@ set_program_and_options:
- cooking_common_program_hood_automatic
- cooking_common_program_hood_venting
- cooking_common_program_hood_delayed_shut_off
- cooking_oven_program_heating_mode_3_d_heating
- cooking_oven_program_heating_mode_3_d_hot_air
- cooking_oven_program_heating_mode_air_fry
- cooking_oven_program_heating_mode_grill_large_area
- cooking_oven_program_heating_mode_grill_small_area
@@ -210,6 +210,7 @@ set_program_and_options:
mode: box
unit_of_measurement: "%"
heating_ventilation_air_conditioning_air_conditioner_option_fan_speed_mode:
example: heating_ventilation_air_conditioning_air_conditioner_enum_type_fan_speed_mode_automatic
required: false
selector:
select:
@@ -222,7 +223,7 @@ set_program_and_options:
collapsed: true
fields:
consumer_products_cleaning_robot_option_reference_map_id:
example: consumer_products_cleaning_robot_enum_type_available_maps_map1
example: consumer_products_cleaning_robot_enum_type_available_maps_map_1
required: false
selector:
select:
@@ -230,9 +231,9 @@ set_program_and_options:
translation_key: available_maps
options:
- consumer_products_cleaning_robot_enum_type_available_maps_temp_map
- consumer_products_cleaning_robot_enum_type_available_maps_map1
- consumer_products_cleaning_robot_enum_type_available_maps_map2
- consumer_products_cleaning_robot_enum_type_available_maps_map3
- consumer_products_cleaning_robot_enum_type_available_maps_map_1
- consumer_products_cleaning_robot_enum_type_available_maps_map_2
- consumer_products_cleaning_robot_enum_type_available_maps_map_3
consumer_products_cleaning_robot_option_cleaning_mode:
example: consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
required: false
@@ -310,7 +311,7 @@ set_program_and_options:
- consumer_products_coffee_maker_enum_type_coffee_temperature_94_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_95_c
- consumer_products_coffee_maker_enum_type_coffee_temperature_96_c
consumer_products_coffee_maker_option_bean_container:
consumer_products_coffee_maker_option_bean_container_selection:
example: consumer_products_coffee_maker_enum_type_bean_container_selection_right
required: false
selector:
@@ -468,8 +469,8 @@ set_program_and_options:
hood_options:
collapsed: true
fields:
cooking_hood_option_venting_level:
example: cooking_hood_enum_type_stage_fan_stage01
cooking_common_option_hood_venting_level:
example: cooking_hood_enum_type_stage_fan_stage_01
required: false
selector:
select:
@@ -482,8 +483,8 @@ set_program_and_options:
- cooking_hood_enum_type_stage_fan_stage_03
- cooking_hood_enum_type_stage_fan_stage_04
- cooking_hood_enum_type_stage_fan_stage_05
cooking_hood_option_intensive_level:
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
cooking_common_option_hood_intensive_level:
example: cooking_hood_enum_type_intensive_stage_intensive_stage_1
required: false
selector:
select:
@@ -491,8 +492,8 @@ set_program_and_options:
translation_key: intensive_level
options:
- cooking_hood_enum_type_intensive_stage_intensive_stage_off
- cooking_hood_enum_type_intensive_stage_intensive_stage1
- cooking_hood_enum_type_intensive_stage_intensive_stage2
- cooking_hood_enum_type_intensive_stage_intensive_stage_1
- cooking_hood_enum_type_intensive_stage_intensive_stage_2
oven_options:
collapsed: true
fields:
@@ -567,7 +568,7 @@ set_program_and_options:
- laundry_care_washer_enum_type_temperature_ul_hot
- laundry_care_washer_enum_type_temperature_ul_extra_hot
laundry_care_washer_option_spin_speed:
example: laundry_care_washer_enum_type_spin_speed_r_p_m800
example: laundry_care_washer_enum_type_spin_speed_r_p_m_800
required: false
selector:
select:
@@ -611,12 +612,12 @@ set_program_and_options:
required: false
selector:
boolean:
laundry_care_washer_option_i_dos1_active:
laundry_care_washer_option_i_dos_1_active:
example: false
required: false
selector:
boolean:
laundry_care_washer_option_i_dos2_active:
laundry_care_washer_option_i_dos_2_active:
example: false
required: false
selector:
@@ -656,7 +657,7 @@ set_program_and_options:
required: false
selector:
boolean:
laundry_care_washer_option_vario_perfect:
laundry_care_common_option_vario_perfect:
example: laundry_care_common_enum_type_vario_perfect_eco_perfect
required: false
selector:

View File

@@ -260,7 +260,7 @@
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
"cooking_oven_program_heating_mode_3_d_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_heating%]",
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
"cooking_oven_program_heating_mode_bread_baking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bread_baking%]",
@@ -431,7 +431,7 @@
}
},
"bean_container": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_container::name%]",
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_coffee_maker_option_bean_container_selection::name%]",
"state": {
"consumer_products_coffee_maker_enum_type_bean_container_selection_left": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_left%]",
"consumer_products_coffee_maker_enum_type_bean_container_selection_right": "[%key:component::home_connect::selector::bean_container::options::consumer_products_coffee_maker_enum_type_bean_container_selection_right%]"
@@ -484,9 +484,9 @@
"current_map": {
"name": "Current map",
"state": {
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_1%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_2%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_3%]",
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]"
}
},
@@ -557,19 +557,19 @@
}
},
"intensive_level": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_intensive_level::name%]",
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_common_option_hood_intensive_level::name%]",
"state": {
"cooking_hood_enum_type_intensive_stage_intensive_stage1": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage1%]",
"cooking_hood_enum_type_intensive_stage_intensive_stage2": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage2%]",
"cooking_hood_enum_type_intensive_stage_intensive_stage_1": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_1%]",
"cooking_hood_enum_type_intensive_stage_intensive_stage_2": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_2%]",
"cooking_hood_enum_type_intensive_stage_intensive_stage_off": "[%key:component::home_connect::selector::intensive_level::options::cooking_hood_enum_type_intensive_stage_intensive_stage_off%]"
}
},
"reference_map_id": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::consumer_products_cleaning_robot_option_reference_map_id::name%]",
"state": {
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map1%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map2%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map3%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_1%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_2%]",
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_map_3%]",
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "[%key:component::home_connect::selector::available_maps::options::consumer_products_cleaning_robot_enum_type_available_maps_temp_map%]"
}
},
@@ -620,7 +620,7 @@
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
"cooking_oven_program_heating_mode_3_d_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_heating%]",
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
"cooking_oven_program_heating_mode_bread_baking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bread_baking%]",
@@ -786,7 +786,7 @@
}
},
"vario_perfect": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_vario_perfect::name%]",
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_common_option_vario_perfect::name%]",
"state": {
"laundry_care_common_enum_type_vario_perfect_eco_perfect": "[%key:component::home_connect::selector::vario_perfect::options::laundry_care_common_enum_type_vario_perfect_eco_perfect%]",
"laundry_care_common_enum_type_vario_perfect_off": "[%key:common::state::off%]",
@@ -794,7 +794,7 @@
}
},
"venting_level": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_hood_option_venting_level::name%]",
"name": "[%key:component::home_connect::services::set_program_and_options::fields::cooking_common_option_hood_venting_level::name%]",
"state": {
"cooking_hood_enum_type_stage_fan_off": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_off%]",
"cooking_hood_enum_type_stage_fan_stage_01": "[%key:component::home_connect::selector::venting_level::options::cooking_hood_enum_type_stage_fan_stage_01%]",
@@ -1272,10 +1272,10 @@
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_hygiene_plus::name%]"
},
"i_dos1_active": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos1_active::name%]"
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos_1_active::name%]"
},
"i_dos2_active": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos2_active::name%]"
"name": "[%key:component::home_connect::services::set_program_and_options::fields::laundry_care_washer_option_i_dos_2_active::name%]"
},
"intensiv_zone": {
"name": "[%key:component::home_connect::services::set_program_and_options::fields::dishcare_dishwasher_option_intensiv_zone::name%]"
@@ -1458,9 +1458,9 @@
},
"available_maps": {
"options": {
"consumer_products_cleaning_robot_enum_type_available_maps_map1": "Map 1",
"consumer_products_cleaning_robot_enum_type_available_maps_map2": "Map 2",
"consumer_products_cleaning_robot_enum_type_available_maps_map3": "Map 3",
"consumer_products_cleaning_robot_enum_type_available_maps_map_1": "Map 1",
"consumer_products_cleaning_robot_enum_type_available_maps_map_2": "Map 2",
"consumer_products_cleaning_robot_enum_type_available_maps_map_3": "Map 3",
"consumer_products_cleaning_robot_enum_type_available_maps_temp_map": "Temporary map"
}
},
@@ -1584,8 +1584,8 @@
},
"intensive_level": {
"options": {
"cooking_hood_enum_type_intensive_stage_intensive_stage1": "Intensive stage 1",
"cooking_hood_enum_type_intensive_stage_intensive_stage2": "Intensive stage 2",
"cooking_hood_enum_type_intensive_stage_intensive_stage_1": "Intensive stage 1",
"cooking_hood_enum_type_intensive_stage_intensive_stage_2": "Intensive stage 2",
"cooking_hood_enum_type_intensive_stage_intensive_stage_off": "Intensive stage off"
}
},
@@ -1629,7 +1629,7 @@
"cooking_common_program_hood_automatic": "Automatic",
"cooking_common_program_hood_delayed_shut_off": "Delayed shut off",
"cooking_common_program_hood_venting": "Venting",
"cooking_oven_program_heating_mode_3_d_heating": "3D heating",
"cooking_oven_program_heating_mode_3_d_hot_air": "3D hot air",
"cooking_oven_program_heating_mode_air_fry": "Air fry",
"cooking_oven_program_heating_mode_bottom_heating": "Bottom heating",
"cooking_oven_program_heating_mode_bread_baking": "Bread baking",
@@ -1892,7 +1892,7 @@
"description": "Describes the amount of coffee beans used in a coffee machine program.",
"name": "Bean amount"
},
"consumer_products_coffee_maker_option_bean_container": {
"consumer_products_coffee_maker_option_bean_container_selection": {
"description": "Defines the preferred bean container.",
"name": "Bean container"
},
@@ -1920,11 +1920,11 @@
"description": "Defines if double dispensing is enabled.",
"name": "Multiple beverages"
},
"cooking_hood_option_intensive_level": {
"cooking_common_option_hood_intensive_level": {
"description": "Defines the intensive setting.",
"name": "Intensive level"
},
"cooking_hood_option_venting_level": {
"cooking_common_option_hood_venting_level": {
"description": "Defines the required fan setting.",
"name": "Venting level"
},
@@ -1992,15 +1992,19 @@
"description": "Defines if the silent mode is activated.",
"name": "Silent mode"
},
"laundry_care_common_option_vario_perfect": {
"description": "Defines if a cycle saves energy (Eco Perfect) or time (Speed Perfect).",
"name": "Vario perfect"
},
"laundry_care_dryer_option_drying_target": {
"description": "Describes the drying target for a dryer program.",
"name": "Drying target"
},
"laundry_care_washer_option_i_dos1_active": {
"laundry_care_washer_option_i_dos_1_active": {
"description": "Defines if the detergent feed is activated / deactivated. (i-Dos content 1)",
"name": "i-Dos 1 Active"
},
"laundry_care_washer_option_i_dos2_active": {
"laundry_care_washer_option_i_dos_2_active": {
"description": "Defines if the detergent feed is activated / deactivated. (i-Dos content 2)",
"name": "i-Dos 2 Active"
},
@@ -2044,10 +2048,6 @@
"description": "Defines the temperature of the washing program.",
"name": "Temperature"
},
"laundry_care_washer_option_vario_perfect": {
"description": "Defines if a cycle saves energy (Eco Perfect) or time (Speed Perfect).",
"name": "Vario perfect"
},
"laundry_care_washer_option_water_plus": {
"description": "Defines if the water plus option is activated.",
"name": "Water +"

View File

@@ -10,6 +10,6 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["aiohue"],
"requirements": ["aiohue==4.8.0"],
"requirements": ["aiohue==4.8.1"],
"zeroconf": ["_hue._tcp.local."]
}

View File

@@ -12,5 +12,5 @@
"iot_class": "local_polling",
"loggers": ["incomfortclient"],
"quality_scale": "platinum",
"requirements": ["incomfort-client==0.6.12"]
"requirements": ["incomfort-client==0.7.0"]
}

View File

@@ -92,11 +92,13 @@
"central_heating": "Central heating",
"central_heating_low": "Central heating low",
"central_heating_rf": "Central heating rf",
"central_heating_wait": "Central heating waiting",
"cv_temperature_too_high_e1": "Temperature too high",
"flame_detection_fault_e6": "Flame detection fault",
"frost": "Frost protection",
"gas_valve_relay_faulty_e29": "Gas valve relay faulty",
"gas_valve_relay_faulty_e30": "[%key:component::incomfort::entity::water_heater::boiler::state::gas_valve_relay_faulty_e29%]",
"hp_error_recovery": "Heat pump error recovery",
"incorrect_fan_speed_e8": "Incorrect fan speed",
"no_flame_signal_e4": "No flame signal",
"off": "[%key:common::state::off%]",
@@ -120,6 +122,7 @@
"service": "Service",
"shortcut_outside_sensor_temperature_e27": "Shortcut outside temperature sensor",
"standby": "[%key:common::state::standby%]",
"starting_ch": "Starting central heating",
"tapwater": "Tap water",
"tapwater_int": "Tap water internal",
"unknown": "Unknown"

View File

@@ -143,7 +143,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: IntellifireConfigEntry)
try:
fireplace: UnifiedFireplace = (
await UnifiedFireplace.build_fireplace_from_common(
_construct_common_data(entry)
_construct_common_data(entry),
polling_enabled=False,
)
)
LOGGER.debug("Waiting for Fireplace to Initialize")

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import aiohttp
from intellifire4py import UnifiedFireplace
from intellifire4py.control import IntelliFireController
from intellifire4py.model import IntelliFirePollData
@@ -11,8 +12,9 @@ from intellifire4py.read import IntelliFireDataProvider
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER
@@ -52,6 +54,14 @@ class IntellifireDataUpdateCoordinator(DataUpdateCoordinator[IntelliFirePollData
return self.fireplace.control_api
async def _async_update_data(self) -> IntelliFirePollData:
try:
await self.fireplace.perform_poll()
except aiohttp.ClientResponseError as err:
if err.status == 403:
raise ConfigEntryAuthFailed("Authentication failed") from err
raise UpdateFailed(f"Error communicating with fireplace: {err}") from err
except (aiohttp.ClientError, TimeoutError) as err:
raise UpdateFailed(f"Error communicating with fireplace: {err}") from err
return self.fireplace.data
@property

View File

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

View File

@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
"""Representation of an LG soundbar device."""
_attr_should_poll = False
_attr_state = MediaPlayerState.OFF
_attr_state = MediaPlayerState.ON # Default to ON to ensure compatibility with models that don't send a powerstatus message
_attr_supported_features = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE

View File

@@ -16,5 +16,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "platinum",
"requirements": ["pylitterbot==2025.2.0"]
"requirements": ["pylitterbot==2025.2.1"]
}

View File

@@ -4,11 +4,20 @@ from dataclasses import dataclass
import logging
from typing import Any, cast
from pylutron import Button, Keypad, Led, Lutron, OccupancyGroup, Output
from pylutron import (
Button,
Keypad,
Led,
Lutron,
LutronException,
OccupancyGroup,
Output,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from .const import DOMAIN
@@ -57,8 +66,12 @@ async def async_setup_entry(
pwd = config_entry.data[CONF_PASSWORD]
lutron_client = Lutron(host, uid, pwd)
await hass.async_add_executor_job(lutron_client.load_xml_db)
lutron_client.connect()
try:
await hass.async_add_executor_job(lutron_client.load_xml_db)
lutron_client.connect()
except LutronException as ex:
raise ConfigEntryNotReady(f"Failed to connect to Lutron repeater: {ex}") from ex
_LOGGER.debug("Connected to main repeater at %s", host)
entity_registry = er.async_get(hass)

View File

@@ -7,6 +7,6 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["pylutron"],
"requirements": ["pylutron==0.4.0"],
"requirements": ["pylutron==0.4.1"],
"single_config_entry": true
}

View File

@@ -116,6 +116,7 @@ SUPPORT_DRY_MODE_DEVICES: set[tuple[int, int]] = {
(0x1209, 0x8027),
(0x1209, 0x8028),
(0x1209, 0x8029),
(0x138C, 0x0101),
}
SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
@@ -156,6 +157,7 @@ SUPPORT_FAN_MODE_DEVICES: set[tuple[int, int]] = {
(0x1209, 0x8028),
(0x1209, 0x8029),
(0x131A, 0x1000),
(0x138C, 0x0101),
}
SystemModeEnum = clusters.Thermostat.Enums.SystemModeEnum

View File

@@ -323,7 +323,11 @@ DISCOVERY_SCHEMAS = [
required_attributes=(
clusters.FanControl.Attributes.FanMode,
clusters.FanControl.Attributes.PercentCurrent,
clusters.FanControl.Attributes.PercentSetting,
),
# PercentSetting SHALL be null when FanMode is Auto (spec 4.4.6.3),
# so allow null values to not block discovery in that state.
allow_none_value=True,
optional_attributes=(
clusters.FanControl.Attributes.SpeedSetting,
clusters.FanControl.Attributes.RockSetting,

View File

@@ -168,10 +168,15 @@ class MatterWaterHeater(MatterEntity, WaterHeaterEntity):
self._attr_target_temperature = self._get_temperature_in_degrees(
clusters.Thermostat.Attributes.OccupiedHeatingSetpoint
)
system_mode = self.get_matter_attribute_value(
clusters.Thermostat.Attributes.SystemMode
)
boost_state = self.get_matter_attribute_value(
clusters.WaterHeaterManagement.Attributes.BoostState
)
if boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
if system_mode == clusters.Thermostat.Enums.SystemModeEnum.kOff:
self._attr_current_operation = STATE_OFF
elif boost_state == clusters.WaterHeaterManagement.Enums.BoostStateEnum.kActive:
self._attr_current_operation = STATE_HIGH_DEMAND
else:
self._attr_current_operation = STATE_ECO
@@ -218,6 +223,7 @@ DISCOVERY_SCHEMAS = [
clusters.Thermostat.Attributes.AbsMinHeatSetpointLimit,
clusters.Thermostat.Attributes.AbsMaxHeatSetpointLimit,
clusters.Thermostat.Attributes.LocalTemperature,
clusters.Thermostat.Attributes.SystemMode,
clusters.WaterHeaterManagement.Attributes.FeatureMap,
),
optional_attributes=(

View File

@@ -260,7 +260,7 @@
},
"clear_playlist": {
"description": "Removes all items from a media player's playlist.",
"name": "Clear playlist"
"name": "Clear media player playlist"
},
"join": {
"description": "Groups media players together for synchronous playback. Only works on supported multiroom audio systems.",
@@ -270,44 +270,44 @@
"name": "Group members"
}
},
"name": "Join"
"name": "Join media players"
},
"media_next_track": {
"description": "Selects the next track.",
"name": "Next"
"description": "Selects the next track on a media player.",
"name": "Next track"
},
"media_pause": {
"description": "Pauses playback on a media player.",
"name": "[%key:common::action::pause%]"
"name": "Pause media"
},
"media_play": {
"description": "Starts playback on a media player.",
"name": "Play"
"name": "Play media"
},
"media_play_pause": {
"description": "Toggles play/pause on a media player.",
"name": "Play/Pause"
"name": "Play/Pause media"
},
"media_previous_track": {
"description": "Selects the previous track.",
"name": "Previous"
"description": "Selects the previous track on a media player.",
"name": "Previous track"
},
"media_seek": {
"description": "Allows you to go to a different part of the media that is currently playing.",
"description": "Allows you to go to a different part of the media that is currently playing on a media player.",
"fields": {
"seek_position": {
"description": "Target position in the currently playing media. The format is platform dependent.",
"name": "Position"
}
},
"name": "Seek"
"name": "Seek media"
},
"media_stop": {
"description": "Stops playback on a media player.",
"name": "[%key:common::action::stop%]"
"name": "Stop media"
},
"play_media": {
"description": "Starts playing specified media.",
"description": "Starts playing specified media on a media player.",
"fields": {
"announce": {
"description": "If the media should be played as an announcement.",
@@ -325,14 +325,14 @@
"name": "Play media"
},
"repeat_set": {
"description": "Sets the repeat mode.",
"description": "Sets the repeat mode of a media player.",
"fields": {
"repeat": {
"description": "Whether the media (one or all) should be played in a loop or not.",
"name": "Repeat mode"
}
},
"name": "Set repeat"
"name": "Set media player repeat"
},
"search_media": {
"description": "Searches the available media.",
@@ -357,14 +357,14 @@
"name": "Search media"
},
"select_sound_mode": {
"description": "Selects a specific sound mode.",
"description": "Selects a specific sound mode of a media player.",
"fields": {
"sound_mode": {
"description": "Name of the sound mode to switch to.",
"name": "Sound mode"
}
},
"name": "Select sound mode"
"name": "Select media player sound mode"
},
"select_source": {
"description": "Sends a media player the command to change the input source.",
@@ -374,37 +374,37 @@
"name": "Source"
}
},
"name": "Select source"
"name": "Select media player source"
},
"shuffle_set": {
"description": "Enables or disables the shuffle mode.",
"description": "Enables or disables the shuffle mode of a media player.",
"fields": {
"shuffle": {
"description": "Whether the media should be played in randomized order or not.",
"name": "Shuffle mode"
}
},
"name": "Set shuffle"
"name": "Set media player shuffle"
},
"toggle": {
"description": "Toggles a media player on/off.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle media player"
},
"turn_off": {
"description": "Turns off the power of a media player.",
"name": "[%key:common::action::turn_off%]"
"name": "Turn off media player"
},
"turn_on": {
"description": "Turns on the power of a media player.",
"name": "[%key:common::action::turn_on%]"
"name": "Turn on media player"
},
"unjoin": {
"description": "Removes a media player from a group. Only works on platforms which support player groups.",
"name": "Unjoin"
"name": "Unjoin media player"
},
"volume_down": {
"description": "Turns down the volume of a media player.",
"name": "Turn down volume"
"name": "Turn down media player volume"
},
"volume_mute": {
"description": "Mutes or unmutes a media player.",
@@ -414,7 +414,7 @@
"name": "Muted"
}
},
"name": "Mute/unmute volume"
"name": "Mute/unmute media player"
},
"volume_set": {
"description": "Sets the volume level of a media player.",
@@ -424,11 +424,11 @@
"name": "Level"
}
},
"name": "Set volume"
"name": "Set media player volume"
},
"volume_up": {
"description": "Turns up the volume of a media player.",
"name": "Turn up volume"
"name": "Turn up media player volume"
}
},
"title": "Media player",

View File

@@ -19,9 +19,13 @@ LIGHT = "light"
LIGHT_ON = 1
LIGHT_OFF = 2
# API "no reading" sentinels. Most temperatures use centidegrees (-32768 -> -327.68 °C).
# Some devices report the int16 minimum already in degrees after scaling (-3276800 raw -> -32768 °C).
DISABLED_TEMP_ENTITIES = (
-32768 / 100,
-32766 / 100,
-32768.0,
-32766.0,
)
@@ -494,7 +498,7 @@ class DishWasherProgramId(MieleEnum, missing_to_none=True):
intensive = 1, 26, 205
maintenance = 2, 27, 214
eco = 3, 22, 28, 200
automatic = 6, 7, 31, 32, 202
automatic = 6, 7, 31, 32, 201, 202
solar_save = 9, 34
gentle = 10, 35, 210
extra_quiet = 11, 36, 207

View File

@@ -93,7 +93,14 @@ def _convert_temperature(
"""Convert temperature object to readable value."""
if index >= len(value_list):
return None
raw_value = cast(int, value_list[index].temperature) / 100.0
raw = value_list[index].temperature
if raw is None:
return None
try:
raw_centi = int(raw)
except TypeError, ValueError:
return None
raw_value = raw_centi / 100.0
if raw_value in DISABLED_TEMP_ENTITIES:
return None
return raw_value
@@ -639,6 +646,7 @@ SENSOR_TYPES: Final[tuple[MieleSensorDefinition[MieleDevice], ...]] = (
MieleAppliance.OVEN,
MieleAppliance.OVEN_MICROWAVE,
MieleAppliance.STEAM_OVEN_COMBI,
MieleAppliance.STEAM_OVEN_MK2,
),
description=MieleSensorDescription(
key="state_core_temperature",
@@ -840,9 +848,9 @@ async def async_setup_entry(
and definition.description.value_fn(device) is None
and definition.description.zone != 1
):
# all appliances supporting temperature have at least zone 1, for other zones
# don't create entity if API signals that datapoint is disabled, unless the sensor
# already appeared in the past (= it provided a valid value)
# Optional temperature datapoints (extra fridge zones, oven food probe): only
# create the entity after the API first reports a valid reading, then keep it
# so state can return to unknown when the datapoint is inactive.
return _is_entity_registered(unique_id)
if (
definition.description.key == "state_plate_step"

View File

@@ -146,7 +146,6 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
_entity_id_format = ENTITY_ID_FORMAT
_attributes_extra_blocked = MQTT_LIGHT_ATTRIBUTES_BLOCKED
_fixed_color_mode: ColorMode | str | None = None
_flash_times: dict[str, int | None]
_topic: dict[str, str | None]
_optimistic: bool
@@ -190,6 +189,7 @@ class MqttLightJson(MqttEntity, LightEntity, RestoreEntity):
self._attr_supported_features |= (
config[CONF_TRANSITION] and LightEntityFeature.TRANSITION
)
self._attr_color_mode = ColorMode.UNKNOWN
if supported_color_modes := self._config.get(CONF_SUPPORTED_COLOR_MODES):
self._attr_supported_color_modes = supported_color_modes
if self.supported_color_modes and len(self.supported_color_modes) == 1:

View File

@@ -104,12 +104,8 @@ async def async_setup_entry(
def _create_entity(device: dict) -> MyNeoSelect:
"""Create a select entity for a device."""
if device["model"] == "EWS":
# According to the MyNeomitis API, EWS "relais" devices expose a "relayMode"
# field in their state, while "pilote" devices do not. We therefore use the
# presence of "relayMode" as an explicit heuristic to distinguish relais
# from pilote devices. If the upstream API changes this behavior, this
# detection logic must be revisited.
if "relayMode" in device.get("state", {}):
state = device.get("state") or {}
if state.get("deviceType") == 0:
description = SELECT_TYPES["relais"]
else:
description = SELECT_TYPES["pilote"]

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["pynintendoauth", "pynintendoparental"],
"quality_scale": "bronze",
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.3"]
"requirements": ["pynintendoauth==1.0.2", "pynintendoparental==2.3.4"]
}

View File

@@ -30,12 +30,12 @@ def _validate_input(data: dict[str, Any]) -> None:
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
nzbget_api = NZBGetAPI(
data[CONF_HOST],
data.get(CONF_USERNAME),
data.get(CONF_PASSWORD),
data[CONF_SSL],
data[CONF_VERIFY_SSL],
data[CONF_PORT],
host=data[CONF_HOST],
username=data.get(CONF_USERNAME),
password=data.get(CONF_PASSWORD),
secure=data[CONF_SSL],
verify_certificate=data[CONF_VERIFY_SSL],
port=data[CONF_PORT],
)
nzbget_api.version()

View File

@@ -35,12 +35,12 @@ class NZBGetDataUpdateCoordinator(DataUpdateCoordinator):
) -> None:
"""Initialize global NZBGet data updater."""
self.nzbget = NZBGetAPI(
config_entry.data[CONF_HOST],
config_entry.data.get(CONF_USERNAME),
config_entry.data.get(CONF_PASSWORD),
config_entry.data[CONF_SSL],
config_entry.data[CONF_VERIFY_SSL],
config_entry.data[CONF_PORT],
host=config_entry.data[CONF_HOST],
username=config_entry.data.get(CONF_USERNAME),
password=config_entry.data.get(CONF_PASSWORD),
secure=config_entry.data[CONF_SSL],
verify_certificate=config_entry.data[CONF_VERIFY_SSL],
port=config_entry.data[CONF_PORT],
)
self._completed_downloads_init = False

View File

@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["oasatelematics"],
"quality_scale": "legacy",
"requirements": ["oasatelematics==0.3"]
"requirements": ["oasatelematics==0.4"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "platinum",
"requirements": ["opower==0.18.0"]
"requirements": ["opower==0.18.1"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_picnic_api2"],
"requirements": ["python-picnic-api2==1.3.1"]
"requirements": ["python-picnic-api2==1.3.4"]
}

View File

@@ -168,15 +168,34 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
docker_version,
docker_info,
docker_system_df,
stacks,
) = await asyncio.gather(
self.portainer.get_containers(endpoint.id),
self.portainer.docker_version(endpoint.id),
self.portainer.docker_info(endpoint.id),
self.portainer.docker_system_df(endpoint.id),
self.portainer.get_stacks(endpoint.id),
)
stack_requests = [self.portainer.get_stacks(endpoint_id=endpoint.id)]
swarm_id = (
docker_info.swarm.cluster.get("ID")
if docker_info.swarm
and docker_info.swarm.control_available
and docker_info.swarm.cluster
else None
)
if swarm_id:
stack_requests.append(
self.portainer.get_stacks(
endpoint_id=endpoint.id, swarm_id=swarm_id
)
)
stacks = [
stack
for result in await asyncio.gather(*stack_requests)
for stack in result
]
prev_endpoint = self.data.get(endpoint.id) if self.data else None
container_map: dict[str, PortainerContainerData] = {}
stack_map: dict[str, PortainerStackData] = {

View File

@@ -189,6 +189,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ProxmoxConfigEntry) ->
# Migration for additional configuration options added to support API tokens
if entry.version < 3:
data = dict(entry.data)
# If CONF_REALM wasn't there yet, extract from username
if CONF_REALM not in data:
data[CONF_REALM] = DEFAULT_REALM
if "@" in data.get(CONF_USERNAME, ""):
username, realm = data[CONF_USERNAME].split("@", 1)
data[CONF_USERNAME] = username
data[CONF_REALM] = realm.lower()
realm = data[CONF_REALM].lower()
# If the realm is one of the base providers, set the provider to match the realm.

View File

@@ -28,14 +28,17 @@ from .coordinator import ProxmoxConfigEntry, ProxmoxCoordinator, ProxmoxNodeData
from .entity import ProxmoxContainerEntity, ProxmoxNodeEntity, ProxmoxVMEntity
from .helpers import is_granted
NO_PERM_VM_LXC_POWER = "no_permission_vm_lxc_power"
@dataclass(frozen=True, kw_only=True)
class ProxmoxNodeButtonNodeEntityDescription(ButtonEntityDescription):
"""Class to hold Proxmox node button description."""
press_action: Callable[[ProxmoxCoordinator, str], None]
permission: ProxmoxPermission = ProxmoxPermission.POWER
permission: ProxmoxPermission = ProxmoxPermission.SYSPOWER
permission_raise: str = "no_permission_node_power"
permission_target: str = "nodes"
@dataclass(frozen=True, kw_only=True)
@@ -44,7 +47,8 @@ class ProxmoxVMButtonEntityDescription(ButtonEntityDescription):
press_action: Callable[[ProxmoxCoordinator, str, int], None]
permission: ProxmoxPermission = ProxmoxPermission.POWER
permission_raise: str = "no_permission_vm_lxc_power"
permission_raise: str = NO_PERM_VM_LXC_POWER
permission_target: str = "vms"
@dataclass(frozen=True, kw_only=True)
@@ -53,7 +57,8 @@ class ProxmoxContainerButtonEntityDescription(ButtonEntityDescription):
press_action: Callable[[ProxmoxCoordinator, str, int], None]
permission: ProxmoxPermission = ProxmoxPermission.POWER
permission_raise: str = "no_permission_vm_lxc_power"
permission_raise: str = NO_PERM_VM_LXC_POWER
permission_target: str = "vms"
NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
@@ -76,6 +81,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
ProxmoxNodeButtonNodeEntityDescription(
key="start_all",
translation_key="start_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
).startall.post(),
@@ -84,6 +92,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
ProxmoxNodeButtonNodeEntityDescription(
key="stop_all",
translation_key="stop_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
).stopall.post(),
@@ -92,6 +103,9 @@ NODE_BUTTONS: tuple[ProxmoxNodeButtonNodeEntityDescription, ...] = (
ProxmoxNodeButtonNodeEntityDescription(
key="suspend_all",
translation_key="suspend_all",
permission=ProxmoxPermission.POWER,
permission_raise=NO_PERM_VM_LXC_POWER,
permission_target="vms",
press_action=lambda coordinator, node: coordinator.proxmox.nodes(
node
).suspendall.post(),
@@ -327,7 +341,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
node_id = self._node_data.node["node"]
if not is_granted(
self.coordinator.permissions,
p_type="nodes",
p_type=self.entity_description.permission_target,
p_id=node_id,
permission=self.entity_description.permission,
):
@@ -352,7 +366,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
vmid = self.vm_data["vmid"]
if not is_granted(
self.coordinator.permissions,
p_type="vms",
p_type=self.entity_description.permission_target,
p_id=vmid,
permission=self.entity_description.permission,
):
@@ -379,7 +393,7 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
# Container power actions fall under vms
if not is_granted(
self.coordinator.permissions,
p_type="vms",
p_type=self.entity_description.permission_target,
p_id=vmid,
permission=self.entity_description.permission,
):

View File

@@ -21,7 +21,7 @@ VM_CONTAINER_RUNNING = "running"
STORAGE_ACTIVE = 1
STORAGE_SHARED = 1
STORAGE_ENABLED = 1
STATUS_OK = "ok"
STATUS_OK = "OK"
AUTH_PAM = "pam"
AUTH_PVE = "pve"
@@ -41,3 +41,4 @@ class ProxmoxPermission(StrEnum):
POWER = "VM.PowerMgmt"
SNAPSHOT = "VM.Snapshot"
SYSPOWER = "Sys.PowerMgmt"

View File

@@ -141,7 +141,7 @@
"name": "Reset"
},
"shutdown": {
"name": "Shutdown"
"name": "Shut down"
},
"snapshot_create": {
"name": "Create snapshot"
@@ -313,7 +313,7 @@
"message": "No active nodes were found on the Proxmox VE server."
},
"no_permission_node_power": {
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'VM.PowerMgmt' permission and try again."
"message": "The configured Proxmox VE user does not have permission to manage the power state of nodes. Please grant the user the 'Sys.PowerMgmt' permission and try again."
},
"no_permission_snapshot": {
"message": "The configured Proxmox VE user does not have permission to create snapshots of VMs and containers. Please grant the user the 'VM.Snapshot' permission and try again."

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aiopvpc"],
"requirements": ["aiopvpc==4.2.2"]
"requirements": ["aiopvpc==4.3.1"]
}

View File

@@ -79,8 +79,10 @@ class QbusLight(QbusEntity, LightEntity):
await self._async_publish_output_state(state)
async def _handle_state_received(self, state: QbusMqttAnalogState) -> None:
percentage = round(state.read_percentage())
self._set_state(percentage)
percentage = state.read_percentage()
if percentage is not None:
self._set_state(round(percentage))
def _set_state(self, percentage: int) -> None:
self._attr_is_on = percentage > 0

View File

@@ -14,5 +14,5 @@
"cloudapp/QBUSMQTTGW/+/state"
],
"quality_scale": "bronze",
"requirements": ["qbusmqttapi==1.4.2"]
"requirements": ["qbusmqttapi==1.4.3"]
}

View File

@@ -10,7 +10,14 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_TYPE
from .const import CONF_CLOUD_ID, CONF_HARDWARE_ADDRESS, CONF_INSTALL_CODE, DOMAIN
from .const import (
CONF_CLOUD_ID,
CONF_HARDWARE_ADDRESS,
CONF_INSTALL_CODE,
DOMAIN,
TYPE_EAGLE_100,
TYPE_EAGLE_200,
)
from .data import CannotConnect, InvalidAuth, async_get_type
_LOGGER = logging.getLogger(__name__)
@@ -63,11 +70,32 @@ class RainforestEagleConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
user_input[CONF_TYPE] = eagle_type
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
return self.async_create_entry(
title=user_input[CONF_CLOUD_ID], data=user_input
)
# Verify it is a known device, first
if not eagle_type:
errors["base"] = "unknown_device_type"
elif eagle_type == TYPE_EAGLE_100:
user_input[CONF_TYPE] = eagle_type
# For EAGLE-100, there is no hardware address to select, so set it to None and move on
user_input[CONF_HARDWARE_ADDRESS] = None
elif eagle_type == TYPE_EAGLE_200:
user_input[CONF_TYPE] = eagle_type
# For EAGLE-200, a connected meter's hardware address is required to create the entry
if not hardware_address:
# hardware_address will be None if there are no meters at all or if none are currently Connected
errors["base"] = "no_meters_connected"
else:
user_input[CONF_HARDWARE_ADDRESS] = hardware_address
else:
# This is a device that isn't supported, yet, but was detected by async_get_type
errors["base"] = "unsupported_device_type"
# All information gathering is done, so if there are no errors at this point, create the entry
if not errors:
return self.async_create_entry(
title=user_input[CONF_CLOUD_ID], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=create_schema(user_input), errors=errors

View File

@@ -34,7 +34,7 @@ class InvalidAuth(RainforestError):
async def async_get_type(hass, cloud_id, install_code, host):
"""Try API call 'get_network_info' to see if target device is Eagle-100 or Eagle-200."""
# For EAGLE-200, fetch the hardware address of the meter too.
# For EAGLE-200, fetch the hardware address of the first connected meter, too.
hub = aioeagle.EagleHub(
aiohttp_client.async_get_clientsession(hass), cloud_id, install_code, host=host
)
@@ -50,8 +50,17 @@ async def async_get_type(hass, cloud_id, install_code, host):
if meters is not None:
if meters:
hardware_address = meters[0].hardware_address
# If there is at least one meter, use the first one with a connection status of "Connected"
hardware_address = next(
(
m.hardware_address
for m in meters
if getattr(m, "connection_status", None) == "Connected"
),
None,
)
else:
# If there are no meters (empty list, since None was already checked for), set the hardware address to None
hardware_address = None
return TYPE_EAGLE_200, hardware_address

View File

@@ -6,7 +6,10 @@
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
"no_meters_connected": "No meters are currently connected. Ensure your meter is connected and try again.",
"unknown": "[%key:common::config_flow::error::unknown%]",
"unknown_device_type": "Unable to determine the type of Rainforest Eagle device. Please ensure your device is supported.",
"unsupported_device_type": "This type of Rainforest Eagle device is not supported."
},
"step": {
"user": {

View File

@@ -12,11 +12,11 @@
"services": {
"disable": {
"description": "Stops the recording of events and state changes.",
"name": "[%key:common::action::disable%]"
"name": "Disable Recorder"
},
"enable": {
"description": "Starts the recording of events and state changes.",
"name": "[%key:common::action::enable%]"
"name": "Enable Recorder"
},
"get_statistics": {
"description": "Retrieves statistics data for entities within a specific time period.",
@@ -46,7 +46,7 @@
"name": "Units"
}
},
"name": "Get statistics"
"name": "Get Recorder statistics"
},
"purge": {
"description": "Starts purge task - to clean up old data from your database.",
@@ -64,7 +64,7 @@
"name": "Repack"
}
},
"name": "Purge"
"name": "Purge Recorder database"
},
"purge_entities": {
"description": "Starts a purge task to remove the data related to specific entities from your database.",
@@ -86,7 +86,7 @@
"name": "[%key:component::recorder::services::purge::fields::keep_days::name%]"
}
},
"name": "Purge entities"
"name": "Purge Recorder entities"
}
},
"system_health": {

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/risco",
"iot_class": "local_push",
"loggers": ["pyrisco"],
"requirements": ["pyrisco==0.6.7"]
"requirements": ["pyrisco==0.6.8"]
}

View File

@@ -19,7 +19,11 @@ from homeassistant.components.vacuum import (
VacuumEntityFeature,
)
from homeassistant.core import HomeAssistant, ServiceResponse, callback
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.exceptions import (
HomeAssistantError,
ServiceNotSupported,
ServiceValidationError,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
@@ -484,6 +488,18 @@ class RoborockQ7Vacuum(RoborockCoordinatedEntityB01Q7, StateVacuumEntity):
},
) from err
async def get_maps(self) -> ServiceResponse:
"""Get map information such as map id and room ids."""
raise ServiceNotSupported(DOMAIN, "get_maps", self.entity_id)
async def get_vacuum_current_position(self) -> ServiceResponse:
"""Get the current position of the vacuum from the map."""
raise ServiceNotSupported(DOMAIN, "get_vacuum_current_position", self.entity_id)
async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
"""Set the vacuum to go to a specific position."""
raise ServiceNotSupported(DOMAIN, "set_vacuum_goto_position", self.entity_id)
class RoborockQ10Vacuum(RoborockCoordinatedEntityB01Q10, StateVacuumEntity):
"""Representation of a Roborock Q10 vacuum."""
@@ -654,3 +670,15 @@ class RoborockQ10Vacuum(RoborockCoordinatedEntityB01Q10, StateVacuumEntity):
"command": command,
},
) from err
async def get_maps(self) -> ServiceResponse:
"""Get map information such as map id and room ids."""
raise ServiceNotSupported(DOMAIN, "get_maps", self.entity_id)
async def get_vacuum_current_position(self) -> ServiceResponse:
"""Get the current position of the vacuum from the map."""
raise ServiceNotSupported(DOMAIN, "get_vacuum_current_position", self.entity_id)
async def async_set_vacuum_goto_position(self, x: int, y: int) -> None:
"""Set the vacuum to go to a specific position."""
raise ServiceNotSupported(DOMAIN, "set_vacuum_goto_position", self.entity_id)

View File

@@ -19,7 +19,6 @@ is_option_selected:
required: true
selector:
state:
attribute: options
hide_states:
- unavailable
- unknown

View File

@@ -8,7 +8,7 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN
@@ -31,7 +31,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up 17Track from a config entry."""
session = async_get_clientsession(hass)
session = async_create_clientsession(hass)
client = SeventeenTrackClient(session=session)
try:

View File

@@ -99,5 +99,5 @@ class SeventeenTrackConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def _get_client(self):
session = aiohttp_client.async_get_clientsession(self.hass)
session = aiohttp_client.async_create_clientsession(self.hass)
return SeventeenTrackClient(session=session)

View File

@@ -38,5 +38,5 @@
"iot_class": "cloud_push",
"loggers": ["pysmartthings"],
"quality_scale": "bronze",
"requirements": ["pysmartthings==3.7.2"]
"requirements": ["pysmartthings==3.7.3"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["pysmhi"],
"requirements": ["pysmhi==1.1.0"]
"requirements": ["pysmhi==2.0.0"]
}

View File

@@ -56,6 +56,21 @@ FORESTDRY_MAP = {
"5": "very_dry",
"6": "extremely_dry",
}
PRECIPITATION_CATEGORY_MAP = {
0: "no_precipitation",
1: "rain",
2: "thunderstorm",
3: "freezing_rain",
4: "mixed_ice",
5: "snow",
6: "wet_snow",
7: "rain_snow_mixed",
8: "ice_pellets",
9: "graupel",
10: "hail",
11: "drizzle",
12: "freezing_drizzle",
}
def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
@@ -68,6 +83,14 @@ def get_percentage_values(entity: SMHIWeatherSensor, key: str) -> int | None:
return None
def get_precipitation_category(entity: SMHIWeatherSensor) -> str | None:
"""Return the precipitation category."""
value: int | None = entity.coordinator.current.get("precipitation_category")
if value in PRECIPITATION_CATEGORY_MAP:
return PRECIPITATION_CATEGORY_MAP[value]
return None
def get_fire_index_value(entity: SMHIFireSensor, key: str) -> str:
"""Return index value as string."""
value: int | None = entity.coordinator.fire_current.get(key) # type: ignore[assignment]
@@ -128,11 +151,9 @@ WEATHER_SENSOR_DESCRIPTIONS: tuple[SMHIWeatherEntityDescription, ...] = (
SMHIWeatherEntityDescription(
key="precipitation_category",
translation_key="precipitation_category",
value_fn=lambda entity: str(
get_percentage_values(entity, "precipitation_category")
),
value_fn=get_precipitation_category,
device_class=SensorDeviceClass.ENUM,
options=["0", "1", "2", "3", "4", "5", "6"],
options=[*PRECIPITATION_CATEGORY_MAP.values()],
),
SMHIWeatherEntityDescription(
key="frozen_precipitation",

View File

@@ -95,13 +95,19 @@
"precipitation_category": {
"name": "Precipitation category",
"state": {
"0": "No precipitation",
"1": "Snow",
"2": "Snow and rain",
"3": "Rain",
"4": "Drizzle",
"5": "Freezing rain",
"6": "Freezing drizzle"
"drizzle": "Drizzle",
"freezing_drizzle": "Freezing drizzle",
"freezing_rain": "Freezing rain",
"graupel": "Graupel",
"hail": "Hail",
"ice_pellets": "Ice pellets",
"mixed_ice": "Mixed/ice",
"no_precipitation": "No precipitation",
"rain": "Rain",
"rain_snow_mixed": "Mixture of rain and snow",
"snow": "Snow",
"thunderstorm": "Thunderstorm",
"wet_snow": "Wet snow"
}
},
"rate_of_spread": {

View File

@@ -417,6 +417,7 @@ class SonosDiscoveryManager:
)
new_coordinator.setup(soco)
c_dict[soco.household_id] = new_coordinator
c_dict[soco.household_id].add_speaker(soco)
speaker.setup(self.entry)
except (OSError, SoCoException, Timeout) as ex:
_LOGGER.warning("Failed to add SonosSpeaker using %s: %s", soco, ex)

View File

@@ -99,3 +99,7 @@ class SonosAlarms(SonosHouseholdCoordinator):
)
self.last_processed_event_id = self.alarms.last_id
return True
def add_speaker(self, soco: SoCo) -> None:
"""Update any skipped alarms when speaker is added."""
self.alarms.update_skipped(soco)

View File

@@ -85,3 +85,6 @@ class SonosHouseholdCoordinator:
def update_cache(self, soco: SoCo, update_id: int | None = None) -> bool:
"""Update the cache of the household-level feature and return if cache has changed."""
raise NotImplementedError
def add_speaker(self, soco: SoCo) -> None:
"""Additional processing when a speaker is added if needed."""

View File

@@ -12,7 +12,7 @@
"quality_scale": "bronze",
"requirements": [
"defusedxml==0.7.1",
"soco==0.30.14",
"soco==0.30.15",
"sonos-websocket==0.1.3"
],
"ssdp": [

View File

@@ -132,7 +132,8 @@ class SonosMedia:
self.artist = track_info.get("artist")
self.album_name = track_info.get("album")
self.title = track_info.get("title")
title = track_info.get("title") or ""
self.title = title.strip() or None
self.image_url = track_info.get("album_art")
playlist_position = int(track_info.get("playlist_position", -1))

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/starlink",
"integration_type": "device",
"iot_class": "local_polling",
"requirements": ["starlink-grpc-core==1.2.4"]
"requirements": ["starlink-grpc-core==1.2.5"]
}

View File

@@ -13,6 +13,7 @@ from switchbot_api import (
SwitchBotAPI,
SwitchBotAuthenticationError,
SwitchBotConnectionError,
SwitchBotDeviceOfflineError,
)
from homeassistant.components import webhook
@@ -202,7 +203,7 @@ async def make_device_data(
if isinstance(device, Device) and device.device_type == "Bot":
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
hass, entry, api, device, coordinators_by_id, True
)
devices_data.sensors.append((device, coordinator))
if coordinator.data is not None:
@@ -405,42 +406,49 @@ async def _initialize_webhook(
hass,
entry.data[CONF_WEBHOOK_ID],
)
# check if webhook is configured in switchbot cloud
check_webhook_result = None
with contextlib.suppress(Exception):
check_webhook_result = await api.get_webook_configuration()
actual_webhook_urls = (
check_webhook_result["urls"]
if check_webhook_result and "urls" in check_webhook_result
else []
)
need_add_webhook = (
len(actual_webhook_urls) == 0 or webhook_url not in actual_webhook_urls
)
need_clean_previous_webhook = (
len(actual_webhook_urls) > 0 and webhook_url not in actual_webhook_urls
)
try:
check_webhook_result = None
with contextlib.suppress(Exception):
check_webhook_result = await api.get_webook_configuration()
if need_clean_previous_webhook:
# it seems is impossible to register multiple webhook.
# So, if webhook already exists, we delete it
await api.delete_webhook(actual_webhook_urls[0])
_LOGGER.debug(
"Deleted previous Switchbot cloud webhook url: %s",
actual_webhook_urls[0],
actual_webhook_urls = (
check_webhook_result["urls"]
if check_webhook_result and "urls" in check_webhook_result
else []
)
need_add_webhook = (
len(actual_webhook_urls) == 0 or webhook_url not in actual_webhook_urls
)
need_clean_previous_webhook = (
len(actual_webhook_urls) > 0 and webhook_url not in actual_webhook_urls
)
if need_add_webhook:
# call api for register webhookurl
await api.setup_webhook(webhook_url)
_LOGGER.debug("Registered Switchbot cloud webhook at hass: %s", webhook_url)
if need_clean_previous_webhook:
# it seems is impossible to register multiple webhook.
# So, if webhook already exists, we delete it
await api.delete_webhook(actual_webhook_urls[0])
_LOGGER.debug(
"Deleted previous Switchbot cloud webhook url: %s",
actual_webhook_urls[0],
)
for coordinator in coordinators_by_id.values():
coordinator.webhook_subscription_listener(True)
if need_add_webhook:
# call api for register webhookurl
await api.setup_webhook(webhook_url)
_LOGGER.debug(
"Registered Switchbot cloud webhook at hass: %s", webhook_url
)
_LOGGER.debug("Registered Switchbot cloud webhook at: %s", webhook_url)
for coordinator in coordinators_by_id.values():
coordinator.webhook_subscription_listener(True)
_LOGGER.debug("Registered Switchbot cloud webhook at: %s", webhook_url)
except SwitchBotDeviceOfflineError as e:
_LOGGER.error("Failed to connect Switchbot cloud device: %s", e)
except SwitchBotConnectionError as e:
_LOGGER.error("Failed to connect Switchbot cloud device: %s", e)
def _create_handle_webhook(

View File

@@ -9,7 +9,7 @@ import os
from typing import TYPE_CHECKING, Any, NamedTuple
from psutil import Process
from psutil._common import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap
from psutil._ntuples import sbattery, sdiskusage, shwtemp, snetio, snicaddr, sswap
import psutil_home_assistant as ha_psutil
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN

View File

@@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/systemmonitor",
"iot_class": "local_push",
"loggers": ["psutil"],
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.1.2"],
"requirements": ["psutil-home-assistant==0.0.1", "psutil==7.2.2"],
"single_config_entry": true
}

View File

@@ -5,7 +5,7 @@ import os
import re
from typing import Any
from psutil._common import sfan, shwtemp
from psutil._ntuples import sfan, shwtemp
import psutil_home_assistant as ha_psutil
from homeassistant.core import HomeAssistant

View File

@@ -52,7 +52,7 @@ VEHICLE_DESCRIPTIONS: tuple[TeslaFleetNumberVehicleEntityDescription, ...] = (
mode=NumberMode.AUTO,
max_key="charge_state_charge_current_request_max",
func=lambda api, value: api.set_charging_amps(value),
scopes=[Scope.VEHICLE_CHARGING_CMDS],
scopes=[Scope.VEHICLE_CHARGING_CMDS, Scope.VEHICLE_CMDS],
),
TeslaFleetNumberVehicleEntityDescription(
key="charge_state_charge_limit_soc",

View File

@@ -30,4 +30,8 @@ class TeslaUserImplementation(AuthImplementation):
@property
def extra_authorize_data(self) -> dict[str, Any]:
"""Extra data that needs to be appended to the authorize url."""
return {"prompt": "login", "scope": " ".join(SCOPES)}
return {
"prompt": "login",
"prompt_missing_scopes": "true",
"scope": " ".join(SCOPES),
}

View File

@@ -50,7 +50,7 @@
"title": "[%key:common::config_flow::title::oauth2_pick_implementation%]"
},
"reauth_confirm": {
"description": "The {name} integration needs to re-authenticate your account",
"description": "The {name} integration needs to re-authenticate your account. Reauthentication refreshes the Tesla API permissions granted to Home Assistant, including any newly enabled scopes.",
"title": "[%key:common::config_flow::title::reauth%]"
},
"registration_complete": {
@@ -60,7 +60,7 @@
"data_description": {
"qr_code": "Scan this QR code with your phone to set up the virtual key."
},
"description": "To enable command signing, you must open the Tesla app, select your vehicle, and then visit the following URL to set up a virtual key. You must repeat this process for each vehicle.\n\n{virtual_key_url}",
"description": "To enable command signing, you must open the Tesla app, select your vehicle, and then visit the following URL to set up a virtual key. You must repeat this process for each vehicle.\n\n{virtual_key_url}\n\nIf you later enable additional Tesla API permissions, reauthenticate the integration to refresh the granted scopes.",
"title": "Command signing"
}
}

View File

@@ -23,7 +23,11 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util, ssl as ssl_util
from .const import AUTH_IMPLEMENTATION, DATA_HASS_CONFIG, DOMAIN, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator
from .coordinator import (
TibberDataAPICoordinator,
TibberDataCoordinator,
TibberPriceCoordinator,
)
from .services import async_setup_services
PLATFORMS = [Platform.BINARY_SENSOR, Platform.NOTIFY, Platform.SENSOR]
@@ -39,6 +43,8 @@ class TibberRuntimeData:
session: OAuth2Session
data_api_coordinator: TibberDataAPICoordinator | None = field(default=None)
data_coordinator: TibberDataCoordinator | None = field(default=None)
price_coordinator: TibberPriceCoordinator | None = field(default=None)
_client: tibber.Tibber | None = None
async def async_get_client(self, hass: HomeAssistant) -> tibber.Tibber:
@@ -55,7 +61,7 @@ class TibberRuntimeData:
time_zone=dt_util.get_default_time_zone(),
ssl=ssl_util.get_default_context(),
)
self._client.set_access_token(access_token)
await self._client.set_access_token(access_token)
return self._client
@@ -124,6 +130,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: TibberConfigEntry) -> bo
except tibber.FatalHttpExceptionError as err:
raise ConfigEntryNotReady("Fatal HTTP error from Tibber API") from err
if tibber_connection.get_homes(only_active=True):
price_coordinator = TibberPriceCoordinator(hass, entry)
await price_coordinator.async_config_entry_first_refresh()
entry.runtime_data.price_coordinator = price_coordinator
data_coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
await data_coordinator.async_config_entry_first_refresh()
entry.runtime_data.data_coordinator = data_coordinator
coordinator = TibberDataAPICoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data.data_api_coordinator = coordinator

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
import random
from typing import TYPE_CHECKING, TypedDict, cast
from aiohttp.client_exceptions import ClientError
@@ -271,9 +272,10 @@ class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
name=f"{DOMAIN} price",
update_interval=timedelta(minutes=1),
)
self._tomorrow_price_poll_threshold_seconds = random.uniform(0, 3600 * 10)
def _seconds_until_next_15_minute(self) -> float:
"""Return seconds until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
def _time_until_next_15_minute(self) -> timedelta:
"""Return time until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
now = dt_util.utcnow()
next_minute = ((now.minute // 15) + 1) * 15
if next_minute >= 60:
@@ -284,7 +286,7 @@ class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
next_run = now.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
return (next_run - now).total_seconds()
return next_run - now
async def _async_update_data(self) -> dict[str, TibberHomeData]:
"""Update data via API and return per-home data for sensors."""
@@ -292,22 +294,44 @@ class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
self.hass
)
active_homes = tibber_connection.get_homes(only_active=True)
now = dt_util.now()
today_start = dt_util.start_of_local_day(now)
today_end = today_start + timedelta(days=1)
tomorrow_start = today_end
tomorrow_end = tomorrow_start + timedelta(days=1)
def _has_prices_today(home: tibber.TibberHome) -> bool:
"""Return True if the home has any prices today."""
for start in home.price_total:
start_dt = dt_util.as_local(datetime.fromisoformat(str(start)))
if today_start <= start_dt < today_end:
return True
return False
def _has_prices_tomorrow(home: tibber.TibberHome) -> bool:
"""Return True if the home has any prices tomorrow."""
for start in home.price_total:
start_dt = dt_util.as_local(datetime.fromisoformat(str(start)))
if tomorrow_start <= start_dt < tomorrow_end:
return True
return False
def _needs_update(home: tibber.TibberHome) -> bool:
"""Return True if the home needs to be updated."""
if not _has_prices_today(home):
return True
if _has_prices_tomorrow(home):
return False
if (today_end - now).total_seconds() < (
self._tomorrow_price_poll_threshold_seconds
):
return True
return False
homes_to_update = [home for home in active_homes if _needs_update(home)]
try:
await asyncio.gather(
tibber_connection.fetch_consumption_data_active_homes(),
tibber_connection.fetch_production_data_active_homes(),
)
now = dt_util.now()
homes_to_update = [
home
for home in active_homes
if (
(last_data_timestamp := home.last_data_timestamp) is None
or (last_data_timestamp - now).total_seconds() < 11 * 3600
)
]
if homes_to_update:
await asyncio.gather(
*(home.update_info_and_price_info() for home in homes_to_update)
@@ -319,7 +343,7 @@ class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
result = {home.home_id: _build_home_data(home) for home in active_homes}
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
self.update_interval = self._time_until_next_15_minute()
return result

View File

@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["tibber"],
"requirements": ["pyTibber==0.36.0"]
"requirements": ["pyTibber==0.37.0"]
}

View File

@@ -609,8 +609,8 @@ async def _async_setup_graphql_sensors(
entity_registry = er.async_get(hass)
coordinator: TibberDataCoordinator | None = None
price_coordinator: TibberPriceCoordinator | None = None
coordinator = entry.runtime_data.data_coordinator
price_coordinator = entry.runtime_data.price_coordinator
entities: list[TibberSensor] = []
for home in tibber_connection.get_homes(only_active=False):
try:
@@ -626,12 +626,9 @@ async def _async_setup_graphql_sensors(
_LOGGER.error("Error connecting to Tibber home: %s ", err)
raise PlatformNotReady from err
if home.has_active_subscription:
if price_coordinator is None:
price_coordinator = TibberPriceCoordinator(hass, entry)
if price_coordinator is not None and home.has_active_subscription:
entities.append(TibberSensorElPrice(price_coordinator, home))
if coordinator is None:
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
if coordinator is not None and home.has_active_subscription:
entities.extend(
TibberDataSensor(home, coordinator, entity_description)
for entity_description in SENSORS
@@ -772,9 +769,15 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
self._model = "Price Sensor"
self._device_name = self._home_name
self._update_attributes()
@callback
def _handle_coordinator_update(self) -> None:
self._update_attributes()
super()._handle_coordinator_update()
@callback
def _update_attributes(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data
if not data or (
@@ -782,7 +785,6 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
or (current_price := home_data.get("current_price")) is None
):
self._attr_available = False
self.async_write_ha_state()
return
self._attr_native_unit_of_measurement = home_data.get(
@@ -804,7 +806,6 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
"estimated_annual_consumption"
]
self._attr_available = True
self.async_write_ha_state()
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):

View File

@@ -6,6 +6,8 @@ import datetime as dt
from datetime import datetime
from typing import TYPE_CHECKING, Any, Final
import aiohttp
import tibber
import voluptuous as vol
from homeassistant.core import (
@@ -15,7 +17,7 @@ from homeassistant.core import (
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.util import dt as dt_util
from .const import DOMAIN
@@ -52,7 +54,52 @@ async def __get_prices(call: ServiceCall) -> ServiceResponse:
tibber_prices: dict[str, Any] = {}
now = dt_util.now()
today_start = dt_util.start_of_local_day(now)
today_end = today_start + dt.timedelta(days=1)
tomorrow_end = today_start + dt.timedelta(days=2)
def _has_valid_prices(home: tibber.TibberHome) -> bool:
"""Return True if the home has valid prices."""
for price_start in home.price_total:
start_dt = dt_util.as_local(datetime.fromisoformat(str(price_start)))
if now.hour >= 13:
if today_end <= start_dt < tomorrow_end:
return True
elif today_start <= start_dt < today_end:
return True
return False
for tibber_home in tibber_connection.get_homes(only_active=True):
if not _has_valid_prices(tibber_home):
try:
await tibber_home.update_info_and_price_info()
except TimeoutError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="get_prices_timeout",
) from err
except tibber.InvalidLoginError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="get_prices_invalid_login",
) from err
except (
tibber.RetryableHttpExceptionError,
tibber.FatalHttpExceptionError,
) as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="get_prices_communication_failed",
translation_placeholders={"detail": str(err.status)},
) from err
except aiohttp.ClientError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="get_prices_communication_failed",
translation_placeholders={"detail": str(err)},
) from err
home_nickname = tibber_home.name
price_data = [

View File

@@ -235,6 +235,15 @@
"data_api_reauth_required": {
"message": "Reconnect Tibber so Home Assistant can enable the new Tibber Data API features."
},
"get_prices_communication_failed": {
"message": "Could not fetch energy prices from Tibber ({detail})"
},
"get_prices_invalid_login": {
"message": "Could not authenticate with Tibber while fetching prices"
},
"get_prices_timeout": {
"message": "Timeout fetching energy prices from Tibber"
},
"invalid_date": {
"message": "Invalid datetime provided {date}"
},

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["tplink-omada-client==1.5.6"]
"requirements": ["tplink-omada-client==1.5.7"]
}

View File

@@ -246,6 +246,7 @@ class TractiveClient:
):
self._last_hw_time = event["hardware"]["time"]
self._send_hardware_update(event)
self._send_switch_update(event)
if (
"position" in event
and self._last_pos_time != event["position"]["time"]
@@ -302,7 +303,10 @@ class TractiveClient:
for switch, key in SWITCH_KEY_MAP.items():
if switch_data := event.get(key):
payload[switch] = switch_data["active"]
payload[ATTR_POWER_SAVING] = event.get("tracker_state_reason") == "POWER_SAVING"
if hardware := event.get("hardware", {}):
payload[ATTR_POWER_SAVING] = (
hardware.get("power_saving_zone_id") is not None
)
self._dispatch_tracker_event(
TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload
)

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_push",
"loggers": ["aiotractive"],
"requirements": ["aiotractive==1.0.1"]
"requirements": ["aiotractive==1.0.2"]
}

View File

@@ -100,13 +100,11 @@ class TractiveSwitch(TractiveEntity, SwitchEntity):
@callback
def handle_status_update(self, event: dict[str, Any]) -> None:
"""Handle status update."""
if self.entity_description.key not in event:
return
if ATTR_POWER_SAVING in event:
self._attr_available = not event[ATTR_POWER_SAVING]
# We received an event, so the service is online and the switch entities should
# be available.
self._attr_available = not event[ATTR_POWER_SAVING]
self._attr_is_on = event[self.entity_description.key]
if self.entity_description.key in event:
self._attr_is_on = event[self.entity_description.key]
self.async_write_ha_state()

View File

@@ -40,7 +40,7 @@ from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
PLATFORMS = [Platform.SENSOR, Platform.SWITCH]
MIGRATION_NAME_TO_KEY = {
# Sensors

View File

@@ -1,64 +0,0 @@
"""Binary sensor platform for Transmission integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import TransmissionConfigEntry, TransmissionDataUpdateCoordinator
from .entity import TransmissionEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class TransmissionBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Describe a Transmission binary sensor entity."""
is_on_fn: Callable[[TransmissionDataUpdateCoordinator], bool | None]
BINARY_SENSOR_TYPES: tuple[TransmissionBinarySensorEntityDescription, ...] = (
TransmissionBinarySensorEntityDescription(
key="port_forwarding",
translation_key="port_forwarding",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
entity_category=EntityCategory.DIAGNOSTIC,
is_on_fn=lambda coordinator: coordinator.port_forwarding,
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: TransmissionConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Transmission binary sensors from a config entry."""
coordinator = config_entry.runtime_data
async_add_entities(
TransmissionBinarySensor(coordinator, description)
for description in BINARY_SENSOR_TYPES
)
class TransmissionBinarySensor(TransmissionEntity, BinarySensorEntity):
"""Representation of a Transmission binary sensor."""
entity_description: TransmissionBinarySensorEntityDescription
@property
def is_on(self) -> bool | None:
"""Return True if the port is open."""
return self.entity_description.is_on_fn(self.coordinator)

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