Compare commits

..

109 Commits

Author SHA1 Message Date
Franck Nijhof
00d5e89951 2026.3.4 (#166285) 2026-03-24 08:11:42 +01:00
Petro31
557d072a4d Update template light test framework (#164688) 2026-03-24 06:38:58 +00:00
Franck Nijhof
6c3917e927 Bump version to 2026.3.4 2026-03-23 19:24:24 +00:00
Bram Kragten
e895c1b2fd Update frontend to 20260312.1 (#166251) 2026-03-23 19:20:37 +00:00
Matrix
dae971cd98 Bump yolink-api to 0.6.3 (#166232) 2026-03-23 19:20:36 +00:00
Peter Grauvogel
807df50eab Bump greenplanet-energy-api from 0.1.4 to 0.1.10 (#166217) 2026-03-23 19:15:57 +00:00
MarkGodwin
aa05ff03b3 Bump tplink-omada-client to fix breaking changes in Omada API (#166206) 2026-03-23 19:15:56 +00:00
Tommy Goode
622b92682e Fix zwave_js fan speed mapping for GE/Jasco Enbrighten 55258 / ZW4002 (#166169) 2026-03-23 17:46:26 +00:00
J. Nick Koston
a81146a227 Bump oralb-ble to 1.1.0 (#166165) 2026-03-23 17:46:24 +00:00
EnjoyingM
530dcadf19 Bump wolf_comm to 0.0.48 (#166144) 2026-03-23 17:31:22 +00:00
Michael
4aa67ddf22 Fix reload of FRITZ!Box Tools in case of connection issues (#166111) 2026-03-23 17:24:39 +00:00
Josef Zweck
8e95b19c4c Bump aiotedee to 0.2.27 (#166101)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-23 17:24:38 +00:00
Sean O'Keeffe
5558b33600 Add additional miele oven programs (#166100) 2026-03-23 17:24:36 +00:00
Ray Xue
0130ac6770 Bump xiaomi-ble to 1.10.0 (#166099) 2026-03-23 17:24:35 +00:00
tronikos
26d22e4d62 Bump python-google-weather-api to 0.0.6 (#166085) 2026-03-23 17:24:33 +00:00
Jack Boswell
532bc02d66 Update starlink-grpc-core to 1.2.4 (#165882) 2026-03-23 17:24:32 +00:00
Petro31
893eac0e84 Correct validation of scripts in template entities (#165226) 2026-03-23 17:22:39 +00:00
Franck Nijhof
c1bd83c9c0 2026.3.3 (#166076) 2026-03-20 23:01:26 +01:00
TimL
b3c27e9f93 Bump Pysmlight 0.3.1 (#166060) 2026-03-20 20:26:10 +00:00
TimL
92e237ade2 Bump Pysmlight to 0.3.0 (#165658) 2026-03-20 20:26:08 +00:00
Franck Nijhof
cbc573a6b1 Bump version to 2026.3.3 2026-03-20 19:56:30 +00:00
TimL
0c059cfc27 Properly handle buttons of SMLIGHT SLZB-MRxU devices (#166058) 2026-03-20 19:55:55 +00:00
tronikos
143ce9d7b3 Bump opower to 0.17.1 (#166044) 2026-03-20 19:55:17 +00:00
Michael
a6aa837d40 Fix enable/disable device tracking feature during setup of FRITZ!Box Tools (#166027) 2026-03-20 19:52:45 +00:00
Joost Lekkerkerker
c58b4a0066 Don't create fridge setpoint if no range in SmartThings (#166018) 2026-03-20 19:52:43 +00:00
Hai-Nam Nguyen
5155242ba7 Bump hyponcloud from 0.3.0 to 0.9.0 (#166005) 2026-03-20 19:52:42 +00:00
Hai-Nam Nguyen
085680f6bf Fix unit when plant power is above 1000W in Hypontech (#165959)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-20 19:52:41 +00:00
AlCalzone
98ecaaa6d2 Do not use moving states for Multilevel Switch CC v1-3 Z-Wave covers (#165909) 2026-03-20 19:52:39 +00:00
Erwin Douna
5ad199fe16 Proxmox fix restart/reboot action (#165901) 2026-03-20 19:52:38 +00:00
Stefan Agner
413cb98424 Fix Abort exception caught by wrong handler in backup encrypt/decrypt (#165852)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-20 19:52:37 +00:00
Robert Svensson
b38c5bcaf2 Bump axis to v67 (#165840) 2026-03-20 19:52:35 +00:00
Joost Lekkerkerker
fa85dfb3b5 Bump pySmartThings to 3.7.2 (#165810) 2026-03-20 19:52:34 +00:00
Robert Resch
f0c6a035db Bump pyOpenSSL to 26.0.0 (#165770) 2026-03-20 19:52:33 +00:00
Ludovic BOUÉ
3f0c200e56 Fix Matter firmware update detection when version strings are identical (#165509) 2026-03-20 19:52:32 +00:00
Raj Laud
a2259ede28 Fix SmartLithium 8-cell support in victron_ble (#165496)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-20 19:52:30 +00:00
Willem-Jan van Rootselaar
24c2b6fe81 Fix optional static values in bsblan (#165488) 2026-03-20 19:52:29 +00:00
Alex Merkel
efc7350e6f LG Soundbar: Fix incorrect state and outdated track information (#165148) 2026-03-20 19:52:28 +00:00
Khole
5f525fc2a1 Hive: Fix bug in config flow for authentication and device registration (#165061) 2026-03-20 19:52:26 +00:00
Tucker Kern
f619a3e7af Snapcast: Fix incorrect identifier extraction in async_join_players (#165020) 2026-03-20 19:52:25 +00:00
Paul Tarjan
4e43492342 Skip unmapped and watchdog event types in Hikvision NVR event injection (#165009)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 19:52:24 +00:00
Erwin Douna
39e70071d3 Start orphaned entries in normal mode only (#164815)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2026-03-20 19:52:22 +00:00
Tom
6da0936a66 Improve ProxmoxVE permissions validation (#164770) 2026-03-20 19:52:21 +00:00
Martin Ecker
5257702530 Add correct speed fan mapping for Z-Wave GE/Jasco Enbrighten ZWA4013 (#164500) 2026-03-20 19:52:20 +00:00
Daniel Hjelseth Høyer
93da5be052 Fix Tibber update token (#164295)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-20 19:52:18 +00:00
Franck Nijhof
2c47e83342 2026.3.2 (#165675) 2026-03-16 13:23:27 +01:00
Franck Nijhof
e3c6a2184d Bump version to 2026.3.2 2026-03-16 10:27:01 +00:00
Simone Chemelli
0ba0829350 Bump aiocomelit to 2.0.1 (#165663) 2026-03-16 10:25:08 +00:00
Allen Porter
678048e681 Upgrade ical dependency to 13.2.2. (#165642) 2026-03-16 10:25:07 +00:00
Jan Bouwhuis
743eeeae53 Fix MQTT device tracker overrides via JSON state attributes without reset (#165529) 2026-03-16 10:25:05 +00:00
Raj Laud
46555c6d9a Fix victron_ble warning sensor using duplicate alarm translation key (#165502)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-16 10:25:04 +00:00
Simone Chemelli
dbaca0a723 Bump aioamazondevices to 13.0.1 (#165476) 2026-03-16 10:25:02 +00:00
Joost Lekkerkerker
9bb2959029 Bump pySmartThings to 3.7.0 (#165468) 2026-03-16 10:25:01 +00:00
Robert Resch
0304781fa9 Bump orjson to 3.11.7 (#165443) 2026-03-16 10:25:00 +00:00
J. Nick Koston
e081d28aa4 Handle OAuth token request exceptions in Yale setup (#165430) 2026-03-16 10:24:58 +00:00
TheJulianJES
34aa28c72f Bump ZHA to 1.0.2 (#165423) 2026-03-16 10:24:56 +00:00
Bram Kragten
cfa2946db8 Update frontend to 20260312.0 (#165420) 2026-03-16 10:24:55 +00:00
Galorhallen
1b0779347c Update govee local api to 2.4.0 (#165418) 2026-03-16 10:24:54 +00:00
Joost Lekkerkerker
93a281e7af Remove stateclass from timestamp entity in Intellifire (#165403) 2026-03-16 10:24:53 +00:00
Josef Zweck
6b32e27fd3 Bump onedrive-personal-sdk to 0.1.7 (#165401) 2026-03-16 10:24:51 +00:00
Zach Feldman
79928a8c7c August oauth2 exception migration (#165397)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-03-16 10:24:50 +00:00
Steve Easley
9146518e13 Bump pyjvcprojector to 2.0.3 (#165327) 2026-03-16 10:24:48 +00:00
Dan Raper
e9c5172f43 Bump ohme to 1.7.0 (#165318) 2026-03-16 10:24:47 +00:00
TheJulianJES
cce21ad4b9 Bump python-otbr-api to 2.9.0 (#165298) 2026-03-16 10:24:46 +00:00
Simone Chemelli
10ec02ca3c Fix switch set for Vodafone Station (#165273) 2026-03-16 10:18:26 +00:00
Josef Zweck
bdf54491e5 Bump onedrive-personal-sdk to 0.1.6 (#165219) 2026-03-16 10:18:25 +00:00
Bram Kragten
0b05d34238 Add reorder support to area selector (#165211) 2026-03-16 10:18:24 +00:00
Åke Strandberg
4c69a1c5f7 Add missing code for Miele dryer (#165122) 2026-03-16 10:17:00 +00:00
Steve Easley
6f1f56dcaa Bump jvc_projector dependency to 2.0.2 (#165099) 2026-03-16 10:16:59 +00:00
Jordan Harvey
d0b9991232 Bump pyanglianwater to 3.1.1 (#165097) 2026-03-16 10:16:58 +00:00
Artur Pragacz
aacf39be8a Make restore state resilient to extra_restore_state_data errors (#165086) 2026-03-16 10:16:56 +00:00
Erwin Douna
bf055da82c Bump pyportainer to 1.0.33 (#165080) 2026-03-16 10:12:26 +00:00
Erwin Douna
0fb118bcd9 Bump pyportainer 1.0.32 (#164803) 2026-03-16 10:12:25 +00:00
Erwin Douna
954ef7d1f5 Fix forced VERIFY_SSL in Portainer (#165079) 2026-03-16 09:56:32 +00:00
Joakim Plate
b091299320 Update pychromecast to 14.0.10 (#165069) 2026-03-16 09:56:31 +00:00
J. Nick Koston
52483e18b2 Bump yalexs-ble to 3.2.8 (#165018) 2026-03-16 09:56:29 +00:00
AlCalzone
57e8683ed7 Fix cover state updates for legacy Multilevel Switch based Z-Wave covers (#165003) 2026-03-16 09:56:28 +00:00
Simone Chemelli
67faace978 Fix dnd switch status for Alexa Devices (#164953) 2026-03-16 09:56:26 +00:00
Simone Chemelli
e4be64fcb1 Fix wifi switch status and add 100% coverage for Fritz (#164696) 2026-03-16 09:56:25 +00:00
Franck Nijhof
f552b8221f 2026.3.1 (#165001) 2026-03-06 22:10:34 +01:00
Franck Nijhof
55dc5392f9 Bump version to 2026.3.1 2026-03-06 20:37:19 +00:00
Karl Beecken
5b93aeae38 Bump teltasync to 0.2.0 (#164995) 2026-03-06 20:37:03 +00:00
Shay Levy
33610bb1a1 Bump aioswitcher to 6.1.1 (#164981) 2026-03-06 20:37:01 +00:00
Manu
6c3cebe413 Change setpoint step size in IronOS integration (#164979) 2026-03-06 20:37:00 +00:00
Willem-Jan van Rootselaar
5346895d9b Bump python-bsblan to 5.1.2 (#164963) 2026-03-06 20:36:58 +00:00
Willem-Jan van Rootselaar
05c3f08c6c Bump python-bsblan to 5.1.1 (#164591) 2026-03-06 20:36:57 +00:00
Daniel Hjelseth Høyer
1ce025733d Fix energy unit in Homevolt (#164959)
Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2026-03-06 20:35:22 +00:00
Simone Chemelli
1537ea86b8 Bump aiovodafone to 3.1.3 (#164955) 2026-03-06 20:35:21 +00:00
Luke Lashley
ec137870fa Pass in Base Url during Roborock reauth (#164903) 2026-03-06 20:35:20 +00:00
Josef Zweck
816ee7f53e Bump onedrive-personal-sdk to 0.1.5 (#164880) 2026-03-06 20:35:18 +00:00
Petro31
6e7eeec827 Fix 'this' variable in template options flow (#164866) 2026-03-06 20:35:17 +00:00
Marc Mueller
d100477a22 Fix volvo test RuntimeWarning (#164845) 2026-03-06 20:35:16 +00:00
Matthias Alphart
98ac6dd2c1 Fix KNX sensor default attributes for energy and volume DPTs (#164838)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-06 20:35:14 +00:00
John O'Nolan
6b30969f60 Fix Ghost config flow using wrong field name for site UUID (#164836) 2026-03-06 20:35:13 +00:00
Joshua Leaper
e9a6b5d662 Update ness_alarm scan interval to 5 secs (#164835) 2026-03-06 20:35:11 +00:00
Glenn de Haan
f95f3f9982 Add device class to active_liter_lpm sensor (#164809) 2026-03-06 20:35:10 +00:00
epenet
3f884a8cd1 Remove caio from licenses exception list (#164806) 2026-03-06 20:35:09 +00:00
Raphael Hehl
10f284932e Enforce SSRF redirect protection only for connector allowed_protocol_schema_set (#164769)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-03-06 20:35:07 +00:00
Sean O'Keeffe
e1c4e6dc42 more programs for Miele steam ovens (#164768)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-03-06 20:35:06 +00:00
Ian Foster
0976e7de4e Update keyboard_remote dependencies (#164755) 2026-03-06 20:35:05 +00:00
Antonio Mello
ae1012b2f0 Fix IntesisHome outdoor_temp not reported when value is 0.0 (#164703)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 20:35:03 +00:00
TimL
bb7c4faca5 Fix button entity creation for devices with more than two radios (#164699) 2026-03-06 20:35:02 +00:00
Tucker Kern
0b1be61336 Ensure Snapcast client has a valid current group before accessing group attributes. (#164683) 2026-03-06 20:35:00 +00:00
Glenn Waters
3ec44024a2 Hunter Douglas Powerview: Fix missing class in hierarchy. (#164264)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-03-06 20:34:59 +00:00
Joost Lekkerkerker
1200cc5779 Bump spotifyaio to 2.0.2 (#164114)
Co-authored-by: Robert Resch <robert@resch.dev>
2026-03-06 20:34:58 +00:00
Blake Messer
d632931f74 Fix Rain Bird controllers updated by Rain Bird 2.x (#163915) 2026-03-06 20:34:56 +00:00
Franck Nijhof
2f9faa53a1 2026.3.0 (#164757) 2026-03-04 20:17:05 +01:00
Joost Lekkerkerker
718607a758 Revert "Add diagnostics platform to AWS S3 (#164118)" (#164759) 2026-03-04 19:01:47 +01:00
Franck Nijhof
3789156559 Revert "Add diagnostics platform to AWS S3 (#164118)"
This reverts commit 37d2c946e8.
2026-03-04 17:53:29 +00:00
Franck Nijhof
042ce6f2de Bump version to 2026.3.0 2026-03-04 17:30:58 +00:00
192 changed files with 7721 additions and 2968 deletions

View File

@@ -1,6 +1,5 @@
"""Defines a base Alexa Devices entity."""
from aioamazondevices.const.devices import SPEAKER_GROUP_DEVICE_TYPE
from aioamazondevices.structures import AmazonDevice
from homeassistant.helpers.device_registry import DeviceInfo
@@ -25,20 +24,15 @@ class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
"""Initialize the entity."""
super().__init__(coordinator)
self._serial_num = serial_num
model = self.device.model
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, serial_num)},
name=self.device.account_name,
model=model,
model=self.device.model,
model_id=self.device.device_type,
manufacturer=self.device.manufacturer or "Amazon",
hw_version=self.device.hardware_version,
sw_version=(
self.device.software_version
if model != SPEAKER_GROUP_DEVICE_TYPE
else None
),
serial_number=serial_num if model != SPEAKER_GROUP_DEVICE_TYPE else None,
sw_version=self.device.software_version,
serial_number=serial_num,
)
self.entity_description = description
self._attr_unique_id = f"{serial_num}-{description.key}"

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==13.0.0"]
"requirements": ["aioamazondevices==13.0.1"]
}

View File

@@ -101,7 +101,10 @@ class AmazonSwitchEntity(AmazonEntity, SwitchEntity):
assert method is not None
await method(self.device, state)
await self.coordinator.async_request_refresh()
self.coordinator.data[self.device.serial_number].sensors[
self.entity_description.key
].value = state
self.async_write_ha_state()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"]
"requirements": ["pyanglianwater==3.1.1"]
}

View File

@@ -5,7 +5,7 @@ from __future__ import annotations
from pathlib import Path
from typing import cast
from aiohttp import ClientResponseError
from aiohttp import ClientError
from yalexs.exceptions import AugustApiAIOHTTPError
from yalexs.manager.exceptions import CannotConnect, InvalidAuth, RequireValidation
from yalexs.manager.gateway import Config as YaleXSConfig
@@ -13,7 +13,12 @@ from yalexs.manager.gateway import Config as YaleXSConfig
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
)
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers.config_entry_oauth2_flow import (
ImplementationUnavailableError,
@@ -45,11 +50,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: AugustConfigEntry) -> bo
august_gateway = AugustGateway(Path(hass.config.config_dir), session, oauth_session)
try:
await async_setup_august(hass, entry, august_gateway)
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed from err
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
except (
AugustApiAIOHTTPError,
OAuth2TokenRequestError,
ClientError,
CannotConnect,
) as err:
raise ConfigEntryNotReady from err
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.8"]
}

View File

@@ -1,55 +0,0 @@
"""Diagnostics support for AWS S3."""
from __future__ import annotations
import dataclasses
from typing import Any
from homeassistant.components.backup import (
DATA_MANAGER as BACKUP_DATA_MANAGER,
BackupManager,
)
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.core import HomeAssistant
from .const import (
CONF_ACCESS_KEY_ID,
CONF_BUCKET,
CONF_PREFIX,
CONF_SECRET_ACCESS_KEY,
DOMAIN,
)
from .coordinator import S3ConfigEntry
from .helpers import async_list_backups_from_s3
TO_REDACT = (CONF_ACCESS_KEY_ID, CONF_SECRET_ACCESS_KEY)
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: S3ConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
backup_manager: BackupManager = hass.data[BACKUP_DATA_MANAGER]
backups = await async_list_backups_from_s3(
coordinator.client,
bucket=entry.data[CONF_BUCKET],
prefix=entry.data.get(CONF_PREFIX, ""),
)
data = {
"coordinator_data": dataclasses.asdict(coordinator.data),
"config": {
**entry.data,
**entry.options,
},
"backup_agents": [
{"name": agent.name}
for agent in backup_manager.backup_agents.values()
if agent.domain == DOMAIN
],
"backup": [backup.as_dict() for backup in backups],
}
return async_redact_data(data, TO_REDACT)

View File

@@ -43,7 +43,7 @@ rules:
# Gold
devices: done
diagnostics: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: S3 is a cloud service that is not discovered on the network.

View File

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

View File

@@ -246,6 +246,8 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
@@ -332,8 +334,10 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Abort:
raise
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
LOGGER.exception("Unexpected error when encrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size

View File

@@ -32,7 +32,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
from .const import CONF_PASSKEY, DOMAIN
from .const import CONF_PASSKEY, DOMAIN, LOGGER
from .coordinator import BSBLanFastCoordinator, BSBLanSlowCoordinator
from .services import async_setup_services
@@ -52,7 +52,7 @@ class BSBLanData:
client: BSBLAN
device: Device
info: Info
static: StaticState
static: StaticState | None
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@@ -82,11 +82,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
# the connection by fetching firmware version
await bsblan.initialize()
# Fetch device metadata in parallel for faster startup
device, info, static = await asyncio.gather(
# Fetch required device metadata in parallel for faster startup
device, info = await asyncio.gather(
bsblan.device(),
bsblan.info(),
bsblan.static_values(),
)
except BSBLANConnectionError as err:
raise ConfigEntryNotReady(
@@ -111,6 +110,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: BSBLanConfigEntry) -> bo
translation_key="setup_general_error",
) from err
try:
static = await bsblan.static_values()
except (BSBLANError, TimeoutError) as err:
LOGGER.debug(
"Static values not available for %s: %s",
entry.data[CONF_HOST],
err,
)
static = None
# Create coordinators with the already-initialized client
fast_coordinator = BSBLanFastCoordinator(hass, entry, bsblan)
slow_coordinator = BSBLanSlowCoordinator(hass, entry, bsblan)

View File

@@ -90,10 +90,11 @@ class BSBLANClimate(BSBLanEntity, ClimateEntity):
self._attr_unique_id = f"{format_mac(data.device.MAC)}-climate"
# Set temperature range if available, otherwise use Home Assistant defaults
if data.static.min_temp is not None and data.static.min_temp.value is not None:
self._attr_min_temp = data.static.min_temp.value
if data.static.max_temp is not None and data.static.max_temp.value is not None:
self._attr_max_temp = data.static.max_temp.value
if (static := data.static) is not None:
if (min_temp := static.min_temp) is not None and min_temp.value is not None:
self._attr_min_temp = min_temp.value
if (max_temp := static.max_temp) is not None and max_temp.value is not None:
self._attr_max_temp = max_temp.value
self._attr_temperature_unit = data.fast_coordinator.client.get_temperature_unit
@property

View File

@@ -24,7 +24,7 @@ async def async_get_config_entry_diagnostics(
"sensor": data.fast_coordinator.data.sensor.model_dump(),
"dhw": data.fast_coordinator.data.dhw.model_dump(),
},
"static": data.static.model_dump(),
"static": data.static.model_dump() if data.static is not None else None,
}
# Add DHW config and schedule from slow coordinator if available

View File

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

View File

@@ -15,7 +15,7 @@
"integration_type": "hub",
"iot_class": "local_polling",
"loggers": ["casttube", "pychromecast"],
"requirements": ["PyChromecast==14.0.9"],
"requirements": ["PyChromecast==14.0.10"],
"single_config_entry": true,
"zeroconf": ["_googlecast._tcp.local."]
}

View File

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

View File

@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
self._use_tls = user_input[CONF_SSL]
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
self._port = self._determine_port(user_input)

View File

@@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import (
FritzSecurityError,
FritzServiceError,
)
from requests.exceptions import ConnectionError
from homeassistant.const import Platform
@@ -68,6 +69,7 @@ BUTTON_TYPE_WOL = "WakeOnLan"
UPTIME_DEVIATION = 5
FRITZ_EXCEPTIONS = (
ConnectionError,
FritzActionError,
FritzActionFailedError,
FritzConnectionException,

View File

@@ -133,26 +133,20 @@ async def _async_wifi_entities_list(
]
)
_LOGGER.debug("WiFi networks count: %s", wifi_count)
networks: dict = {}
networks: dict[int, dict[str, Any]] = {}
for i in range(1, wifi_count + 1):
network_info = await avm_wrapper.async_get_wlan_configuration(i)
# Devices with 4 WLAN services, use the 2nd for internal communications
if not (wifi_count == 4 and i == 2):
networks[i] = {
"ssid": network_info["NewSSID"],
"bssid": network_info["NewBSSID"],
"standard": network_info["NewStandard"],
"enabled": network_info["NewEnable"],
"status": network_info["NewStatus"],
}
networks[i] = network_info
for i, network in networks.copy().items():
networks[i]["switch_name"] = network["ssid"]
networks[i]["switch_name"] = network["NewSSID"]
if (
len(
[
j
for j, n in networks.items()
if slugify(n["ssid"]) == slugify(network["ssid"])
if slugify(n["NewSSID"]) == slugify(network["NewSSID"])
]
)
> 1
@@ -434,13 +428,11 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch):
for key, attr in attributes_dict.items():
self._attributes[attr] = self.port_mapping[key]
async def _async_switch_on_off_executor(self, turn_on: bool) -> bool:
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
self.port_mapping["NewEnabled"] = "1" if turn_on else "0"
resp = await self._avm_wrapper.async_add_port_mapping(
await self._avm_wrapper.async_add_port_mapping(
self.connection_type, self.port_mapping
)
return bool(resp is not None)
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
@@ -525,12 +517,11 @@ class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
"""Turn off switch."""
await self._async_handle_turn_on_off(turn_on=False)
async def _async_handle_turn_on_off(self, turn_on: bool) -> bool:
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
"""Handle switch state change request."""
await self._avm_wrapper.async_set_allow_wan_access(self.ip_address, turn_on)
self._avm_wrapper.devices[self._mac].wan_access = turn_on
self.async_write_ha_state()
return True
class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
@@ -541,10 +532,11 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
avm_wrapper: AvmWrapper,
device_friendly_name: str,
network_num: int,
network_data: dict,
network_data: dict[str, Any],
) -> None:
"""Init Fritz Wifi switch."""
self._avm_wrapper = avm_wrapper
self._wifi_info = network_data
self._attributes = {}
self._attr_entity_category = EntityCategory.CONFIG
@@ -560,7 +552,7 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
type=SWITCH_TYPE_WIFINETWORK,
callback_update=self._async_fetch_update,
callback_switch=self._async_switch_on_off_executor,
init_state=network_data["enabled"],
init_state=network_data["NewEnable"],
)
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
@@ -587,7 +579,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
self._attributes["mac_address_control"] = wifi_info[
"NewMACAddressControlEnabled"
]
self._wifi_info = wifi_info
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
"""Handle wifi switch."""
self._wifi_info["NewEnable"] = turn_on
await self._avm_wrapper.async_set_wlan_configuration(self._network_num, turn_on)

View File

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

View File

@@ -89,7 +89,7 @@ class GhostConfigFlow(ConfigFlow, domain=DOMAIN):
site_title = site["title"]
await self.async_set_unique_id(site["uuid"])
await self.async_set_unique_id(site["site_uuid"])
self._abort_if_unique_id_configured()
return self.async_create_entry(

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.0"]
"requirements": ["gcal-sync==8.0.0", "oauth2client==4.1.3", "ical==13.2.2"]
}

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["google_weather_api"],
"quality_scale": "bronze",
"requirements": ["python-google-weather-api==0.0.4"]
"requirements": ["python-google-weather-api==0.0.6"]
}

View File

@@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==2.3.0"]
"requirements": ["govee-local-api==2.4.0"]
}

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["greenplanet-energy-api==0.1.4"],
"requirements": ["greenplanet-energy-api==0.1.10"],
"single_config_entry": true
}

View File

@@ -117,13 +117,21 @@ async def async_setup_entry(hass: HomeAssistant, entry: HikvisionConfigEntry) ->
# Map raw event type names to friendly names using SENSOR_MAP
mapped_events: dict[str, list[int]] = {}
for event_type, channels in nvr_events.items():
friendly_name = SENSOR_MAP.get(event_type.lower(), event_type)
event_key = event_type.lower()
# Skip videoloss - used as watchdog by pyhik, not a real sensor
if event_key == "videoloss":
continue
friendly_name = SENSOR_MAP.get(event_key)
if friendly_name is None:
_LOGGER.debug("Skipping unmapped event type: %s", event_type)
continue
if friendly_name in mapped_events:
mapped_events[friendly_name].extend(channels)
else:
mapped_events[friendly_name] = list(channels)
_LOGGER.debug("Mapped NVR events: %s", mapped_events)
camera.inject_events(mapped_events)
if mapped_events:
camera.inject_events(mapped_events)
else:
_LOGGER.debug(
"No event triggers returned from %s. "

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from apyhiveapi import Auth
@@ -26,6 +27,8 @@ from homeassistant.core import callback
from . import HiveConfigEntry
from .const import CONF_CODE, CONF_DEVICE_NAME, CONFIG_ENTRY_VERSION, DOMAIN
_LOGGER = logging.getLogger(__name__)
class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a Hive config flow."""
@@ -36,7 +39,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Initialize the config flow."""
self.data: dict[str, Any] = {}
self.tokens: dict[str, str] = {}
self.tokens: dict[str, Any] = {}
self.device_registration: bool = False
self.device_name = "Home Assistant"
@@ -67,11 +70,22 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
except HiveApiError:
errors["base"] = "no_internet_available"
if (
auth_result := self.tokens.get("AuthenticationResult", {})
) and auth_result.get("NewDeviceMetadata"):
_LOGGER.debug("Login successful, New device detected")
self.device_registration = True
return await self.async_step_configuration()
if self.tokens.get("ChallengeName") == "SMS_MFA":
_LOGGER.debug("Login successful, SMS 2FA required")
# Complete SMS 2FA.
return await self.async_step_2fa()
if not errors:
_LOGGER.debug(
"Login successful, no new device detected, no 2FA required"
)
# Complete the entry.
try:
return await self.async_setup_hive_entry()
@@ -103,6 +117,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
errors["base"] = "no_internet_available"
if not errors:
_LOGGER.debug("2FA successful")
if self.source == SOURCE_REAUTH:
return await self.async_setup_hive_entry()
self.device_registration = True
@@ -119,10 +134,11 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
if user_input:
if self.device_registration:
_LOGGER.debug("Attempting to register device")
self.device_name = user_input["device_name"]
await self.hive_auth.device_registration(user_input["device_name"])
self.data["device_data"] = await self.hive_auth.get_device_data()
_LOGGER.debug("Device registration successful")
try:
return await self.async_setup_hive_entry()
except UnknownHiveError:
@@ -142,6 +158,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
raise UnknownHiveError
# Setup the config entry
_LOGGER.debug("Setting up Hive entry")
self.data["tokens"] = self.tokens
if self.source == SOURCE_REAUTH:
return self.async_update_reload_and_abort(
@@ -160,6 +177,7 @@ class HiveFlowHandler(ConfigFlow, domain=DOMAIN):
CONF_USERNAME: entry_data[CONF_USERNAME],
CONF_PASSWORD: entry_data[CONF_PASSWORD],
}
_LOGGER.debug("Reauthenticating user")
return await self.async_step_user(data)
@staticmethod

View File

@@ -91,14 +91,14 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
translation_key="energy_exported",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="energy_imported",
translation_key="energy_imported",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
SensorEntityDescription(
key="frequency",

View File

@@ -610,6 +610,7 @@ SENSORS: Final[tuple[HomeWizardSensorEntityDescription, ...]] = (
key="active_liter_lpm",
translation_key="active_liter_lpm",
native_unit_of_measurement=UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
state_class=SensorStateClass.MEASUREMENT,
has_fn=lambda data: data.measurement.active_liter_lpm is not None,
value_fn=lambda data: data.measurement.active_liter_lpm,

View File

@@ -901,7 +901,9 @@ class PowerViewShadeDualOverlappedRear(PowerViewShadeDualOverlappedBase):
)
class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombined):
class PowerViewShadeDualOverlappedCombinedTilt(
PowerViewShadeDualOverlappedCombined, PowerViewShadeWithTiltBase
):
"""Represent a shade that has a front sheer and rear opaque panel.
This equates to two shades being controlled by one motor.
@@ -915,26 +917,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
Type 10 - Duolite with 180° Tilt
"""
# type
def __init__(
self,
coordinator: PowerviewShadeUpdateCoordinator,
device_info: PowerviewDeviceInfo,
room_name: str,
shade: BaseShade,
name: str,
) -> None:
"""Initialize the shade."""
super().__init__(coordinator, device_info, room_name, shade, name)
self._attr_supported_features |= (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
| CoverEntityFeature.SET_TILT_POSITION
)
if self._shade.is_supported(MOTION_STOP):
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
self._max_tilt = self._shade.shade_limits.tilt_max
@property
def transition_steps(self) -> int:
"""Return the steps to make a move."""
@@ -949,26 +931,6 @@ class PowerViewShadeDualOverlappedCombinedTilt(PowerViewShadeDualOverlappedCombi
tilt = self.positions.tilt
return ceil(primary + secondary + tilt)
@callback
def _get_shade_tilt(self, target_hass_tilt_position: int) -> ShadePosition:
"""Return a ShadePosition."""
return ShadePosition(
tilt=target_hass_tilt_position,
velocity=self.positions.velocity,
)
@property
def open_tilt_position(self) -> ShadePosition:
"""Return the open tilt position and required additional positions."""
return replace(self._shade.open_position_tilt, velocity=self.positions.velocity)
@property
def close_tilt_position(self) -> ShadePosition:
"""Return the open tilt position and required additional positions."""
return replace(
self._shade.close_position_tilt, velocity=self.positions.velocity
)
TYPE_TO_CLASSES = {
0: (PowerViewShade,),

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["hyponcloud==0.3.0"]
"requirements": ["hyponcloud==0.9.0"]
}

View File

@@ -21,11 +21,17 @@ from .coordinator import HypontechConfigEntry, HypontechDataCoordinator
from .entity import HypontechEntity, HypontechPlantEntity
def _power_unit(data: OverviewData | PlantData) -> str:
"""Return the unit of measurement for power based on the API unit."""
return UnitOfPower.KILO_WATT if data.company.upper() == "KW" else UnitOfPower.WATT
@dataclass(frozen=True, kw_only=True)
class HypontechSensorDescription(SensorEntityDescription):
"""Describes Hypontech overview sensor entity."""
value_fn: Callable[[OverviewData], float | None]
unit_fn: Callable[[OverviewData], str] | None = None
@dataclass(frozen=True, kw_only=True)
@@ -33,15 +39,16 @@ class HypontechPlantSensorDescription(SensorEntityDescription):
"""Describes Hypontech plant sensor entity."""
value_fn: Callable[[PlantData], float | None]
unit_fn: Callable[[PlantData], str] | None = None
OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
HypontechSensorDescription(
key="pv_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.power,
unit_fn=_power_unit,
),
HypontechSensorDescription(
key="lifetime_energy",
@@ -64,10 +71,10 @@ OVERVIEW_SENSORS: tuple[HypontechSensorDescription, ...] = (
PLANT_SENSORS: tuple[HypontechPlantSensorDescription, ...] = (
HypontechPlantSensorDescription(
key="pv_power",
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda c: c.power,
unit_fn=_power_unit,
),
HypontechPlantSensorDescription(
key="lifetime_energy",
@@ -124,6 +131,13 @@ class HypontechOverviewSensor(HypontechEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{coordinator.account_id}_{description.key}"
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self.entity_description.unit_fn is not None:
return self.entity_description.unit_fn(self.coordinator.data.overview)
return super().native_unit_of_measurement
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""
@@ -146,6 +160,13 @@ class HypontechPlantSensor(HypontechPlantEntity, SensorEntity):
self.entity_description = description
self._attr_unique_id = f"{plant_id}_{description.key}"
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self.entity_description.unit_fn is not None:
return self.entity_description.unit_fn(self.plant)
return super().native_unit_of_measurement
@property
def native_value(self) -> float | None:
"""Return the state of the sensor."""

View File

@@ -97,7 +97,6 @@ INTELLIFIRE_SENSORS: tuple[IntellifireSensorEntityDescription, ...] = (
IntellifireSensorEntityDescription(
key="timer_end_timestamp",
translation_key="timer_end_timestamp",
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=_time_remaining_to_timestamp,
),

View File

@@ -221,13 +221,13 @@ class IntesisAC(ClimateEntity):
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the device specific state attributes."""
attrs = {}
if self._outdoor_temp:
if self._outdoor_temp is not None:
attrs["outdoor_temp"] = self._outdoor_temp
if self._power_consumption_heat:
if self._power_consumption_heat is not None:
attrs["power_consumption_heat_kw"] = round(
self._power_consumption_heat / 1000, 1
)
if self._power_consumption_cool:
if self._power_consumption_cool is not None:
attrs["power_consumption_cool_kw"] = round(
self._power_consumption_cool / 1000, 1
)
@@ -244,7 +244,7 @@ class IntesisAC(ClimateEntity):
if hvac_mode := kwargs.get(ATTR_HVAC_MODE):
await self.async_set_hvac_mode(hvac_mode)
if temperature := kwargs.get(ATTR_TEMPERATURE):
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
_LOGGER.debug("Setting %s to %s degrees", self._device_type, temperature)
await self._controller.set_temperature(self._device_id, temperature)
self._attr_target_temperature = temperature
@@ -271,7 +271,7 @@ class IntesisAC(ClimateEntity):
await self._controller.set_mode(self._device_id, MAP_HVAC_MODE_TO_IH[hvac_mode])
# Send the temperature again in case changing modes has changed it
if self._attr_target_temperature:
if self._attr_target_temperature is not None:
await self._controller.set_temperature(
self._device_id, self._attr_target_temperature
)

View File

@@ -358,7 +358,7 @@ PINECIL_SETPOINT_NUMBER_DESCRIPTION = IronOSNumberEntityDescription(
native_max_value=MAX_TEMP,
native_min_value_f=MIN_TEMP_F,
native_max_value_f=MAX_TEMP_F,
native_step=5,
native_step=1,
)

View File

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

View File

@@ -7,5 +7,5 @@
"iot_class": "local_push",
"loggers": ["aionotify", "evdev"],
"quality_scale": "legacy",
"requirements": ["evdev==1.6.1", "asyncinotify==4.2.0"]
"requirements": ["evdev==1.9.3", "asyncinotify==4.4.0"]
}

View File

@@ -8,6 +8,7 @@ from xknx.dpt import DPTBase, DPTComplex, DPTEnum, DPTNumeric
from xknx.dpt.dpt_16 import DPTString
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
from homeassistant.const import UnitOfReactiveEnergy
HaDptClass = Literal["numeric", "enum", "complex", "string"]
@@ -36,7 +37,7 @@ def get_supported_dpts() -> Mapping[str, DPTInfo]:
main=dpt_class.dpt_main_number, # type: ignore[typeddict-item] # checked in xknx unit tests
sub=dpt_class.dpt_sub_number,
name=dpt_class.value_type,
unit=dpt_class.unit,
unit=_sensor_unit_overrides.get(dpt_number_str, dpt_class.unit),
sensor_device_class=_sensor_device_classes.get(dpt_number_str),
sensor_state_class=_get_sensor_state_class(ha_dpt_class, dpt_number_str),
)
@@ -77,13 +78,13 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"12.1200": SensorDeviceClass.VOLUME,
"12.1201": SensorDeviceClass.VOLUME,
"13.002": SensorDeviceClass.VOLUME_FLOW_RATE,
"13.010": SensorDeviceClass.ENERGY,
"13.012": SensorDeviceClass.REACTIVE_ENERGY,
"13.013": SensorDeviceClass.ENERGY,
"13.015": SensorDeviceClass.REACTIVE_ENERGY,
"13.016": SensorDeviceClass.ENERGY,
"13.1200": SensorDeviceClass.VOLUME,
"13.1201": SensorDeviceClass.VOLUME,
"13.010": SensorDeviceClass.ENERGY, # DPTActiveEnergy
"13.012": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergy
"13.013": SensorDeviceClass.ENERGY, # DPTActiveEnergykWh
"13.015": SensorDeviceClass.REACTIVE_ENERGY, # DPTReactiveEnergykVARh
"13.016": SensorDeviceClass.ENERGY, # DPTActiveEnergyMWh
"13.1200": SensorDeviceClass.VOLUME, # DPTDeltaVolumeLiquidLitre
"13.1201": SensorDeviceClass.VOLUME, # DPTDeltaVolumeM3
"14.010": SensorDeviceClass.AREA,
"14.019": SensorDeviceClass.CURRENT,
"14.027": SensorDeviceClass.VOLTAGE,
@@ -91,7 +92,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.030": SensorDeviceClass.VOLTAGE,
"14.031": SensorDeviceClass.ENERGY,
"14.033": SensorDeviceClass.FREQUENCY,
"14.037": SensorDeviceClass.ENERGY_STORAGE,
"14.037": SensorDeviceClass.ENERGY_STORAGE, # DPTHeatQuantity
"14.039": SensorDeviceClass.DISTANCE,
"14.051": SensorDeviceClass.WEIGHT,
"14.056": SensorDeviceClass.POWER,
@@ -101,7 +102,7 @@ _sensor_device_classes: Mapping[str, SensorDeviceClass] = {
"14.068": SensorDeviceClass.TEMPERATURE,
"14.069": SensorDeviceClass.TEMPERATURE,
"14.070": SensorDeviceClass.TEMPERATURE_DELTA,
"14.076": SensorDeviceClass.VOLUME,
"14.076": SensorDeviceClass.VOLUME, # DPTVolume
"14.077": SensorDeviceClass.VOLUME_FLOW_RATE,
"14.080": SensorDeviceClass.APPARENT_POWER,
"14.1200": SensorDeviceClass.VOLUME_FLOW_RATE,
@@ -121,17 +122,28 @@ _sensor_state_class_overrides: Mapping[str, SensorStateClass | None] = {
"13.010": SensorStateClass.TOTAL, # DPTActiveEnergy
"13.011": SensorStateClass.TOTAL, # DPTApparantEnergy
"13.012": SensorStateClass.TOTAL, # DPTReactiveEnergy
"13.013": SensorStateClass.TOTAL, # DPTActiveEnergykWh
"13.015": SensorStateClass.TOTAL, # DPTReactiveEnergykVARh
"13.016": SensorStateClass.TOTAL, # DPTActiveEnergyMWh
"13.1200": SensorStateClass.TOTAL, # DPTDeltaVolumeLiquidLitre
"13.1201": SensorStateClass.TOTAL, # DPTDeltaVolumeM3
"14.007": SensorStateClass.MEASUREMENT_ANGLE, # DPTAngleDeg
"14.037": SensorStateClass.TOTAL, # DPTHeatQuantity
"14.051": SensorStateClass.TOTAL, # DPTMass
"14.055": SensorStateClass.MEASUREMENT_ANGLE, # DPTPhaseAngleDeg
"14.031": SensorStateClass.TOTAL_INCREASING, # DPTEnergy
"14.076": SensorStateClass.TOTAL, # DPTVolume
"17.001": None, # DPTSceneNumber
"29.010": SensorStateClass.TOTAL, # DPTActiveEnergy8Byte
"29.011": SensorStateClass.TOTAL, # DPTApparantEnergy8Byte
"29.012": SensorStateClass.TOTAL, # DPTReactiveEnergy8Byte
}
_sensor_unit_overrides: Mapping[str, str] = {
"13.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy (VARh in KNX)
"13.015": UnitOfReactiveEnergy.KILO_VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergykVARh (kVARh in KNX)
"29.012": UnitOfReactiveEnergy.VOLT_AMPERE_REACTIVE_HOUR, # DPTReactiveEnergy8Byte (VARh in KNX)
}
def _get_sensor_state_class(
ha_dpt_class: HaDptClass, dpt_number_str: str

View File

@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
"""Representation of an LG soundbar device."""
_attr_should_poll = False
_attr_state = MediaPlayerState.ON
_attr_state = MediaPlayerState.OFF
_attr_supported_features = (
MediaPlayerEntityFeature.VOLUME_SET
| MediaPlayerEntityFeature.VOLUME_MUTE
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
self._treble = 0
self._device = None
self._support_play_control = False
self._device_on = False
self._stream_type = 0
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)}, name=host
)
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
if "i_curr_func" in data:
self._function = data["i_curr_func"]
if "b_powerstatus" in data:
self._device_on = data["b_powerstatus"]
if data["b_powerstatus"]:
self._attr_state = MediaPlayerState.ON
else:
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
def _update_playinfo(self, data: dict[str, Any]) -> None:
"""Update the player info."""
if "i_stream_type" in data:
if self._stream_type != data["i_stream_type"]:
self._stream_type = data["i_stream_type"]
# Ask device for current play info when stream type changed.
self._device.get_play()
if data["i_stream_type"] == 0:
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
# the last track info should be cleared and the state should only be on or off,
# as all playing/paused are not applicable in this mode
self._attr_media_image_url = None
self._attr_media_artist = None
self._attr_media_title = None
if self._device_on:
self._attr_state = MediaPlayerState.ON
else:
self._attr_state = MediaPlayerState.OFF
if "i_play_ctrl" in data:
if data["i_play_ctrl"] == 0:
self._attr_state = MediaPlayerState.PLAYING
else:
self._attr_state = MediaPlayerState.PAUSED
if self._device_on and self._stream_type != 0:
if data["i_play_ctrl"] == 0:
self._attr_state = MediaPlayerState.PLAYING
else:
self._attr_state = MediaPlayerState.PAUSED
if "s_albumart" in data:
self._attr_media_image_url = data["s_albumart"]
self._attr_media_image_url = data["s_albumart"].strip() or None
if "s_artist" in data:
self._attr_media_artist = data["s_artist"]
self._attr_media_artist = data["s_artist"].strip() or None
if "s_title" in data:
self._attr_media_title = data["s_title"]
self._attr_media_title = data["s_title"].strip() or None
if "b_support_play_ctrl" in data:
self._support_play_control = data["b_support_play_ctrl"]

View File

@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -80,6 +80,7 @@ class MatterUpdate(MatterEntity, UpdateEntity):
# Matter server.
_attr_should_poll = True
_software_update: MatterSoftwareVersion | None = None
_installed_software_version: int | None = None
_cancel_update: CALLBACK_TYPE | None = None
_attr_supported_features = (
UpdateEntityFeature.INSTALL
@@ -92,6 +93,9 @@ class MatterUpdate(MatterEntity, UpdateEntity):
def _update_from_device(self) -> None:
"""Update from device."""
self._installed_software_version = self.get_matter_attribute_value(
clusters.BasicInformation.Attributes.SoftwareVersion
)
self._attr_installed_version = self.get_matter_attribute_value(
clusters.BasicInformation.Attributes.SoftwareVersionString
)
@@ -123,6 +127,22 @@ class MatterUpdate(MatterEntity, UpdateEntity):
else:
self._attr_update_percentage = None
def _format_latest_version(
self, update_information: MatterSoftwareVersion
) -> str | None:
"""Return the version string to expose in Home Assistant."""
latest_version = update_information.software_version_string
if self._installed_software_version is None:
return latest_version
if update_information.software_version == self._installed_software_version:
return self._attr_installed_version or latest_version
if latest_version == self._attr_installed_version:
return f"{latest_version} ({update_information.software_version})"
return latest_version
async def async_update(self) -> None:
"""Call when the entity needs to be updated."""
try:
@@ -130,11 +150,13 @@ class MatterUpdate(MatterEntity, UpdateEntity):
node_id=self._endpoint.node.node_id
)
if not update_information:
self._software_update = None
self._attr_latest_version = self._attr_installed_version
self._attr_release_url = None
return
self._software_update = update_information
self._attr_latest_version = update_information.software_version_string
self._attr_latest_version = self._format_latest_version(update_information)
self._attr_release_url = update_information.release_notes_url
except UpdateCheckError as err:
@@ -212,7 +234,12 @@ class MatterUpdate(MatterEntity, UpdateEntity):
software_version: str | int | None = version
if self._software_update is not None and (
version is None or version == self._software_update.software_version_string
version is None
or version
in {
self._software_update.software_version_string,
self._attr_latest_version,
}
):
# Update to the version previously fetched and shown.
# We can pass the integer version directly to speedup download.

View File

@@ -188,6 +188,7 @@ class ProgramPhaseTumbleDryer(MieleEnum, missing_to_none=True):
finished = 522, 11012
extra_dry = 523
hand_iron = 524
hygiene_drying = 525
moisten = 526
thermo_spin = 527
timed_drying = 528
@@ -615,13 +616,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pyrolytic = 323
descale = 326
evaporate_water = 327
rinse = 333
shabbat_program = 335
yom_tov = 336
drying = 357
hydroclean = 341
drying = 357, 2028
heat_crockery = 358
prove_dough = 359
prove_dough = 359, 2023
low_temperature_cooking = 360
steam_cooking = 361
steam_cooking = 8, 361
keeping_warm = 362
apple_sponge = 364
apple_pie = 365
@@ -668,9 +671,9 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
saddle_of_roebuck = 456
salmon_fillet = 461
potato_cheese_gratin = 464
trout = 486
carp = 491
salmon_trout = 492
trout = 486, 2224
carp = 491, 2233
salmon_trout = 492, 2241
springform_tin_15cm = 496
springform_tin_20cm = 497
springform_tin_25cm = 498
@@ -721,7 +724,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
belgian_sponge_cake = 624
goose_unstuffed = 625
rack_of_lamb_with_vegetables = 634
yorkshire_pudding = 635
yorkshire_pudding = 635, 2352
meat_loaf = 636
defrost_meat = 647
defrost_vegetables = 654
@@ -736,137 +739,15 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
pork_belly = 701
pikeperch_fillet_with_vegetables = 702
steam_bake = 99001
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for dish warmers."""
no_program = 0, -1
warm_cups_glasses = 1
warm_dishes_plates = 2
keep_warm = 3
slow_roasting = 4
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for robot vacuum cleaners."""
no_program = 0, -1
auto = 1
spot = 2
turbo = 3
silent = 4
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for coffee systems."""
no_program = 0, -1
check_appliance = 17004
# profile 1
ristretto = 24000, 24032, 24064, 24096, 24128
espresso = 24001, 24033, 24065, 24097, 24129
coffee = 24002, 24034, 24066, 24098, 24130
long_coffee = 24003, 24035, 24067, 24099, 24131
cappuccino = 24004, 24036, 24068, 24100, 24132
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
latte_macchiato = 24006, 24038, 24070, 24102, 24134
espresso_macchiato = 24007, 24039, 24071, 24135
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
caffe_latte = 24009, 24041, 24073, 24105, 24137
flat_white = 24012, 24044, 24076, 24108, 24140
very_hot_water = 24013, 24045, 24077, 24109, 24141
hot_water = 24014, 24046, 24078, 24110, 24142
hot_milk = 24015, 24047, 24079, 24111, 24143
milk_foam = 24016, 24048, 24080, 24112, 24144
black_tea = 24017, 24049, 24081, 24113, 24145
herbal_tea = 24018, 24050, 24082, 24114, 24146
fruit_tea = 24019, 24051, 24083, 24115, 24147
green_tea = 24020, 24052, 24084, 24116, 24148
white_tea = 24021, 24053, 24085, 24117, 24149
japanese_tea = 24022, 29054, 24086, 24118, 24150
# special programs
coffee_pot = 24400
barista_assistant = 24407
# machine settings menu
appliance_settings = (
16016, # display brightness
16018, # volume
16019, # buttons volume
16020, # child lock
16021, # water hardness
16027, # welcome sound
16033, # connection status
16035, # remote control
16037, # remote update
24500, # total dispensed
24502, # lights appliance on
24503, # lights appliance off
24504, # turn off lights after
24506, # altitude
24513, # performance mode
24516, # turn off after
24537, # advanced mode
24542, # tea timer
24549, # total coffee dispensed
24550, # total tea dispensed
24551, # total ristretto
24552, # total cappuccino
24553, # total espresso
24554, # total coffee
24555, # total long coffee
24556, # total italian cappuccino
24557, # total latte macchiato
24558, # total caffe latte
24560, # total espresso macchiato
24562, # total flat white
24563, # total coffee with milk
24564, # total black tea
24565, # total herbal tea
24566, # total fruit tea
24567, # total green tea
24568, # total white tea
24569, # total japanese tea
24571, # total milk foam
24572, # total hot milk
24573, # total hot water
24574, # total very hot water
24575, # counter to descaling
24576, # counter to brewing unit degreasing
24800, # maintenance
24801, # profiles settings menu
24813, # add profile
)
appliance_rinse = 24750, 24759, 24773, 24787, 24788
intermediate_rinsing = 24758
automatic_maintenance = 24778
descaling = 24751
brewing_unit_degrease = 24753
milk_pipework_rinse = 24754
milk_pipework_clean = 24789
class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for steam oven micro combo."""
no_program = 0, -1
steam_cooking = 8
microwave = 19
popcorn = 53
quick_mw = 54
sous_vide = 72
eco_steam_cooking = 75
rapid_steam_cooking = 77
descale = 326
menu_cooking = 330
reheating_with_steam = 2018
defrosting_with_steam = 2019
blanching = 2020
bottling = 2021
sterilize_crockery = 2022
prove_dough = 2023
soak = 2027
reheating_with_microwave = 2029
defrosting_with_microwave = 2030
@@ -1020,18 +901,15 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
gilt_head_bream_fillet = 2220
codfish_piece = 2221, 2232
codfish_fillet = 2222, 2231
trout = 2224
pike_fillet = 2225
pike_piece = 2226
halibut_fillet_2_cm = 2227
halibut_fillet_3_cm = 2230
carp = 2233
salmon_fillet_2_cm = 2234
salmon_fillet_3_cm = 2235
salmon_steak_2_cm = 2238
salmon_steak_3_cm = 2239
salmon_piece = 2240
salmon_trout = 2241
iridescent_shark_fillet = 2244
red_snapper_fillet_2_cm = 2245
red_snapper_fillet_3_cm = 2248
@@ -1246,7 +1124,7 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
wholegrain_rice = 3376
parboiled_rice_steam_cooking = 3380
parboiled_rice_rapid_steam_cooking = 3381
basmati_rice_steam_cooking = 3383
basmati_rice_steam_cooking = 3382, 3383
basmati_rice_rapid_steam_cooking = 3384
jasmine_rice_steam_cooking = 3386
jasmine_rice_rapid_steam_cooking = 3387
@@ -1254,7 +1132,7 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
huanghuanian_rapid_steam_cooking = 3390
simiao_steam_cooking = 3392
simiao_rapid_steam_cooking = 3393
long_grain_rice_general_steam_cooking = 3395
long_grain_rice_general_steam_cooking = 3394, 3395
long_grain_rice_general_rapid_steam_cooking = 3396
chongming_steam_cooking = 3398
chongming_rapid_steam_cooking = 3399
@@ -1268,6 +1146,116 @@ class SteamOvenMicroProgramId(MieleEnum, missing_to_none=True):
round_grain_rice_general_rapid_steam_cooking = 3411
class DishWarmerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for dish warmers."""
no_program = 0, -1
warm_cups_glasses = 1
warm_dishes_plates = 2
keep_warm = 3
slow_roasting = 4
class RobotVacuumCleanerProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for robot vacuum cleaners."""
no_program = 0, -1
auto = 1
spot = 2
turbo = 3
silent = 4
class CoffeeSystemProgramId(MieleEnum, missing_to_none=True):
"""Program Id codes for coffee systems."""
no_program = 0, -1
check_appliance = 17004
# profile 1
ristretto = 24000, 24032, 24064, 24096, 24128
espresso = 24001, 24033, 24065, 24097, 24129
coffee = 24002, 24034, 24066, 24098, 24130
long_coffee = 24003, 24035, 24067, 24099, 24131
cappuccino = 24004, 24036, 24068, 24100, 24132
cappuccino_italiano = 24005, 24037, 24069, 24101, 24133
latte_macchiato = 24006, 24038, 24070, 24102, 24134
espresso_macchiato = 24007, 24039, 24071, 24135
cafe_au_lait = 24008, 24040, 24072, 24104, 24136
caffe_latte = 24009, 24041, 24073, 24105, 24137
flat_white = 24012, 24044, 24076, 24108, 24140
very_hot_water = 24013, 24045, 24077, 24109, 24141
hot_water = 24014, 24046, 24078, 24110, 24142
hot_milk = 24015, 24047, 24079, 24111, 24143
milk_foam = 24016, 24048, 24080, 24112, 24144
black_tea = 24017, 24049, 24081, 24113, 24145
herbal_tea = 24018, 24050, 24082, 24114, 24146
fruit_tea = 24019, 24051, 24083, 24115, 24147
green_tea = 24020, 24052, 24084, 24116, 24148
white_tea = 24021, 24053, 24085, 24117, 24149
japanese_tea = 24022, 29054, 24086, 24118, 24150
# special programs
coffee_pot = 24400
barista_assistant = 24407
# machine settings menu
appliance_settings = (
16016, # display brightness
16018, # volume
16019, # buttons volume
16020, # child lock
16021, # water hardness
16027, # welcome sound
16033, # connection status
16035, # remote control
16037, # remote update
24500, # total dispensed
24502, # lights appliance on
24503, # lights appliance off
24504, # turn off lights after
24506, # altitude
24513, # performance mode
24516, # turn off after
24537, # advanced mode
24542, # tea timer
24549, # total coffee dispensed
24550, # total tea dispensed
24551, # total ristretto
24552, # total cappuccino
24553, # total espresso
24554, # total coffee
24555, # total long coffee
24556, # total italian cappuccino
24557, # total latte macchiato
24558, # total caffe latte
24560, # total espresso macchiato
24562, # total flat white
24563, # total coffee with milk
24564, # total black tea
24565, # total herbal tea
24566, # total fruit tea
24567, # total green tea
24568, # total white tea
24569, # total japanese tea
24571, # total milk foam
24572, # total hot milk
24573, # total hot water
24574, # total very hot water
24575, # counter to descaling
24576, # counter to brewing unit degreasing
24800, # maintenance
24801, # profiles settings menu
24813, # add profile
)
appliance_rinse = 24750, 24759, 24773, 24787, 24788
intermediate_rinsing = 24758
automatic_maintenance = 24778
descaling = 24751
brewing_unit_degrease = 24753
milk_pipework_rinse = 24754
milk_pipework_clean = 24789
PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.WASHING_MACHINE: WashingMachineProgramId,
MieleAppliance.TUMBLE_DRYER: TumbleDryerProgramId,
@@ -1278,7 +1266,7 @@ PROGRAM_IDS: dict[int, type[MieleEnum]] = {
MieleAppliance.STEAM_OVEN_MK2: OvenProgramId,
MieleAppliance.STEAM_OVEN: OvenProgramId,
MieleAppliance.STEAM_OVEN_COMBI: OvenProgramId,
MieleAppliance.STEAM_OVEN_MICRO: SteamOvenMicroProgramId,
MieleAppliance.STEAM_OVEN_MICRO: OvenProgramId,
MieleAppliance.WASHER_DRYER: WashingMachineProgramId,
MieleAppliance.ROBOT_VACUUM_CLEANER: RobotVacuumCleanerProgramId,
MieleAppliance.COFFEE_SYSTEM: CoffeeSystemProgramId,

View File

@@ -474,6 +474,7 @@
"drain_spin": "Drain/spin",
"drop_cookies_1_tray": "Drop cookies (1 tray)",
"drop_cookies_2_trays": "Drop cookies (2 trays)",
"drying": "Drying",
"duck": "Duck",
"dutch_hash": "Dutch hash",
"easy_care": "Easy care",
@@ -559,6 +560,7 @@
"hot_water": "Hot water",
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
"hydroclean": "HydroClean",
"hygiene": "Hygiene",
"intensive": "Intensive",
"intensive_bake": "Intensive bake",
@@ -1005,6 +1007,7 @@
"heating_up_phase": "Heating up phase",
"hot_milk": "Hot milk",
"hygiene": "Hygiene",
"hygiene_drying": "Hygiene drying",
"interim_rinse": "Interim rinse",
"keep_warm": "Keep warm",
"keeping_warm": "Keeping warm",

View File

@@ -163,8 +163,6 @@ class MqttDeviceTracker(MqttEntity, TrackerEntity):
latitude: float | None
longitude: float | None
gps_accuracy: float
# Reset manually set location to allow automatic zone detection
self._attr_location_name = None
if isinstance(
latitude := extra_state_attributes.get(ATTR_LATITUDE), (int, float)
) and isinstance(

View File

@@ -24,7 +24,7 @@ SUBENTRY_TYPE_ZONE = "zone"
# Defaults
DEFAULT_PORT = 4999
DEFAULT_SCAN_INTERVAL = timedelta(minutes=1)
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
DEFAULT_INFER_ARMING_STATE = False
DEFAULT_ZONE_TYPE = BinarySensorDeviceClass.MOTION

View File

@@ -19,6 +19,5 @@ async def async_get_config_entry_diagnostics(
return {
"device_info": client.device_info,
"vehicles": client.vehicles,
"ct_connected": client.ct_connected,
"cap_available": client.cap_available,
}

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "platinum",
"requirements": ["ohme==1.6.0"]
"requirements": ["ohme==1.7.0"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -10,5 +10,5 @@
"iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "platinum",
"requirements": ["onedrive-personal-sdk==0.1.4"]
"requirements": ["onedrive-personal-sdk==0.1.7"]
}

View File

@@ -9,5 +9,5 @@
"iot_class": "cloud_polling",
"loggers": ["opower"],
"quality_scale": "bronze",
"requirements": ["opower==0.17.0"]
"requirements": ["opower==0.17.1"]
}

View File

@@ -13,5 +13,5 @@
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["oralb_ble"],
"requirements": ["oralb-ble==1.0.2"]
"requirements": ["oralb-ble==1.1.0"]
}

View File

@@ -8,5 +8,5 @@
"documentation": "https://www.home-assistant.io/integrations/otbr",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.8.0"]
"requirements": ["python-otbr-api==2.9.0"]
}

View File

@@ -159,8 +159,12 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
await self.async_set_unique_id(user_input[CONF_API_TOKEN])
self._abort_if_unique_id_configured()
# Logic that can be reverted back once the new unique ID is in
existing_entry = await self.async_set_unique_id(
user_input[CONF_API_TOKEN]
)
if existing_entry and existing_entry.entry_id != reconf_entry.entry_id:
return self.async_abort(reason="already_configured")
return self.async_update_reload_and_abort(
reconf_entry,
data_updates={

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["pyportainer==1.0.31"]
"requirements": ["pyportainer==1.0.33"]
}

View File

@@ -104,7 +104,7 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
ProxmoxVMButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).qemu(vmid).status.restart.post()
coordinator.proxmox.nodes(node).qemu(vmid).status.reboot.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -147,7 +147,7 @@ CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
ProxmoxContainerButtonEntityDescription(
key="restart",
press_action=lambda coordinator, node, vmid: (
coordinator.proxmox.nodes(node).lxc(vmid).status.restart.post()
coordinator.proxmox.nodes(node).lxc(vmid).status.reboot.post()
),
entity_category=EntityCategory.CONFIG,
device_class=ButtonDeviceClass.RESTART,
@@ -277,7 +277,8 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the node button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="nodes"):
node_id = self._node_data.node["node"]
if not is_granted(self.coordinator.permissions, p_type="nodes", p_id=node_id):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_node_power",
@@ -285,7 +286,7 @@ class ProxmoxNodeButtonEntity(ProxmoxNodeEntity, ProxmoxBaseButton):
await self.hass.async_add_executor_job(
self.entity_description.press_action,
self.coordinator,
self._node_data.node["node"],
node_id,
)
@@ -309,7 +310,8 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the VM button action via executor."""
if not is_granted(self.coordinator.permissions, p_type="vms"):
vmid = self.vm_data["vmid"]
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
@@ -318,7 +320,7 @@ class ProxmoxVMButtonEntity(ProxmoxVMEntity, ProxmoxBaseButton):
self.entity_description.press_action,
self.coordinator,
self._node_name,
self.vm_data["vmid"],
vmid,
)
@@ -342,8 +344,9 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
async def _async_press_call(self) -> None:
"""Execute the container button action via executor."""
vmid = self.container_data["vmid"]
# Container power actions fall under vms
if not is_granted(self.coordinator.permissions, p_type="vms"):
if not is_granted(self.coordinator.permissions, p_type="vms", p_id=vmid):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="no_permission_vm_lxc_power",
@@ -352,5 +355,5 @@ class ProxmoxContainerButtonEntity(ProxmoxContainerEntity, ProxmoxBaseButton):
self.entity_description.press_action,
self.coordinator,
self._node_name,
self.container_data["vmid"],
vmid,
)

View File

@@ -6,8 +6,13 @@ from .const import PERM_POWER
def is_granted(
permissions: dict[str, dict[str, int]],
p_type: str = "vms",
p_id: str | int | None = None, # can be str for nodes
permission: str = PERM_POWER,
) -> bool:
"""Validate user permissions for the given type and permission."""
path = f"/{p_type}"
return permissions.get(path, {}).get(permission) == 1
paths = [f"/{p_type}/{p_id}", f"/{p_type}", "/"]
for path in paths:
value = permissions.get(path, {}).get(permission)
if value is not None:
return value == 1
return False

View File

@@ -2,11 +2,12 @@
from __future__ import annotations
import asyncio
import logging
from typing import Any
import aiohttp
from pyrainbird.async_client import AsyncRainbirdController, CreateController
from pyrainbird.async_client import AsyncRainbirdController, create_controller
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
from homeassistant.const import (
@@ -26,7 +27,7 @@ from homeassistant.helpers import (
from homeassistant.helpers.device_registry import format_mac
from homeassistant.helpers.typing import ConfigType
from .const import CONF_SERIAL_NUMBER, DOMAIN
from .const import CONF_SERIAL_NUMBER, DOMAIN, TIMEOUT_SECONDS
from .coordinator import (
RainbirdScheduleUpdateCoordinator,
RainbirdUpdateCoordinator,
@@ -77,11 +78,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: RainbirdConfigEntry) ->
clientsession = async_create_clientsession()
_async_register_clientsession_shutdown(hass, entry, clientsession)
controller = CreateController(
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
controller = await create_controller(
clientsession,
entry.data[CONF_HOST],
entry.data[CONF_PASSWORD],
)
except TimeoutError as err:
raise ConfigEntryNotReady from err
except RainbirdAuthException as err:
raise ConfigEntryAuthFailed from err
except RainbirdApiException as err:
raise ConfigEntryNotReady from err
if not (await _async_fix_unique_id(hass, controller, entry)):
return False

View File

@@ -7,7 +7,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from pyrainbird.async_client import CreateController
from pyrainbird.async_client import create_controller
from pyrainbird.data import WifiParams
from pyrainbird.exceptions import RainbirdApiException, RainbirdAuthException
import voluptuous as vol
@@ -137,9 +137,9 @@ class RainbirdConfigFlowHandler(ConfigFlow, domain=DOMAIN):
Raises a ConfigFlowError on failure.
"""
clientsession = async_create_clientsession()
controller = CreateController(clientsession, host, password)
try:
async with asyncio.timeout(TIMEOUT_SECONDS):
controller = await create_controller(clientsession, host, password)
return await asyncio.gather(
controller.get_serial_number(),
controller.get_wifi_params(),

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["ical"],
"quality_scale": "silver",
"requirements": ["ical==13.2.0"]
"requirements": ["ical==13.2.2"]
}

View File

@@ -188,7 +188,9 @@ class RoborockFlowHandler(ConfigFlow, domain=DOMAIN):
self._username = entry_data[CONF_USERNAME]
assert self._username
self._client = RoborockApiClient(
self._username, session=async_get_clientsession(self.hass)
self._username,
base_url=entry_data[CONF_BASE_URL],
session=async_get_clientsession(self.hass),
)
return await self.async_step_reauth_confirm()

View File

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

View File

@@ -43,6 +43,10 @@ async def async_setup_entry(
for component in device.status
if component in ("cooler", "freezer", "onedoor")
and Capability.THERMOSTAT_COOLING_SETPOINT in device.status[component]
and device.status[component][Capability.THERMOSTAT_COOLING_SETPOINT][
Attribute.COOLING_SETPOINT_RANGE
].value
is not None
)
async_add_entities(entities)

View File

@@ -1,4 +1,4 @@
"""Support for SLZB-06 buttons."""
"""Support for SLZB buttons."""
from __future__ import annotations
@@ -35,24 +35,25 @@ class SmButtonDescription(ButtonEntityDescription):
press_fn: Callable[[CmdWrapper, int], Awaitable[None]]
BUTTONS: list[SmButtonDescription] = [
SmButtonDescription(
key="core_restart",
translation_key="core_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.reboot(),
),
CORE_BUTTON = SmButtonDescription(
key="core_restart",
translation_key="core_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.reboot(),
)
RADIO_BUTTONS: list[SmButtonDescription] = [
SmButtonDescription(
key="zigbee_restart",
translation_key="zigbee_restart",
device_class=ButtonDeviceClass.RESTART,
press_fn=lambda cmd, idx: cmd.zb_restart(),
press_fn=lambda cmd, idx: cmd.zb_restart(idx=idx),
),
SmButtonDescription(
key="zigbee_flash_mode",
translation_key="zigbee_flash_mode",
entity_registry_enabled_default=False,
press_fn=lambda cmd, idx: cmd.zb_bootloader(),
press_fn=lambda cmd, idx: cmd.zb_bootloader(idx=idx),
),
]
@@ -73,8 +74,14 @@ async def async_setup_entry(
coordinator = entry.runtime_data.data
radios = coordinator.data.info.radios
async_add_entities(SmButton(coordinator, button) for button in BUTTONS)
entity_created = [False, False]
entities = [SmButton(coordinator, CORE_BUTTON)]
count = len(radios) if coordinator.data.info.u_device else 1
for idx in range(count):
entities.extend(SmButton(coordinator, button, idx) for button in RADIO_BUTTONS)
async_add_entities(entities)
entity_created = [False] * len(radios)
@callback
def _check_router(startup: bool = False) -> None:
@@ -103,7 +110,7 @@ async def async_setup_entry(
class SmButton(SmEntity, ButtonEntity):
"""Defines a SLZB-06 button."""
"""Defines a SLZB button."""
coordinator: SmDataUpdateCoordinator
entity_description: SmButtonDescription
@@ -115,7 +122,7 @@ class SmButton(SmEntity, ButtonEntity):
description: SmButtonDescription,
idx: int = 0,
) -> None:
"""Initialize SLZB-06 button entity."""
"""Initialize SLZB button entity."""
super().__init__(coordinator)
self.entity_description = description

View File

@@ -12,7 +12,7 @@
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "silver",
"requirements": ["pysmlight==0.2.16"],
"requirements": ["pysmlight==0.3.1"],
"zeroconf": [
{
"type": "_slzb-06._tcp.local."

View File

@@ -131,7 +131,7 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
return f"{CLIENT_PREFIX}{host}_{id}"
@property
def _current_group(self) -> Snapgroup:
def _current_group(self) -> Snapgroup | None:
"""Return the group the client is associated with."""
return self._device.group
@@ -158,12 +158,17 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
def state(self) -> MediaPlayerState | None:
"""Return the state of the player."""
if self._device.connected:
if self.is_volume_muted or self._current_group.muted:
if (
self.is_volume_muted
or self._current_group is None
or self._current_group.muted
):
return MediaPlayerState.IDLE
try:
return STREAM_STATUS.get(self._current_group.stream_status)
except KeyError:
pass
return MediaPlayerState.OFF
@property
@@ -182,15 +187,31 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def source(self) -> str | None:
"""Return the current input source."""
if self._current_group is None:
return None
return self._current_group.stream
@property
def source_list(self) -> list[str]:
"""List of available input sources."""
if self._current_group is None:
return []
return list(self._current_group.streams_by_name().keys())
async def async_select_source(self, source: str) -> None:
"""Set input source."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="select_source_no_group",
translation_placeholders={
"entity_id": self.entity_id,
"source": source,
},
)
streams = self._current_group.streams_by_name()
if source in streams:
await self._current_group.set_stream(streams[source].identifier)
@@ -233,6 +254,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def group_members(self) -> list[str] | None:
"""List of player entities which are currently grouped together for synchronous playback."""
if self._current_group is None:
return None
entity_registry = er.async_get(self.hass)
return [
entity_id
@@ -248,6 +272,15 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
async def async_join_players(self, group_members: list[str]) -> None:
"""Add `group_members` to this client's current group."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="join_players_no_group",
translation_placeholders={
"entity_id": self.entity_id,
},
)
# Get the client entity for each group member excluding self
entity_registry = er.async_get(self.hass)
clients = [
@@ -257,27 +290,52 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
and entity.unique_id != self.unique_id
]
# Get unique ID prefix for this host
unique_id_prefix = self.get_unique_id(self.coordinator.host_id, "")
for client in clients:
# Valid entity is a snapcast client
# Validate entity is a snapcast client
if not client.unique_id.startswith(CLIENT_PREFIX):
raise ServiceValidationError(
f"Entity '{client.entity_id}' is not a Snapcast client device."
)
# Validate client belongs to the same server
if not client.unique_id.startswith(unique_id_prefix):
raise ServiceValidationError(
f"Entity '{client.entity_id}' does not belong to the same Snapcast server."
)
# Extract client ID and join it to the current group
identifier = client.unique_id.split("_")[-1]
await self._current_group.add_client(identifier)
identifier = client.unique_id.removeprefix(unique_id_prefix)
try:
await self._current_group.add_client(identifier)
except KeyError as e:
raise ServiceValidationError(
f"Client with identifier '{identifier}' does not exist on the server."
) from e
self.async_write_ha_state()
async def async_unjoin_player(self) -> None:
"""Remove this client from it's current group."""
"""Remove this client from its current group."""
if self._current_group is None:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unjoin_no_group",
translation_placeholders={
"entity_id": self.entity_id,
},
)
await self._current_group.remove_client(self._device.identifier)
self.async_write_ha_state()
@property
def metadata(self) -> Mapping[str, Any]:
"""Get metadata from the current stream."""
if self._current_group is None:
return {}
try:
if metadata := self.coordinator.server.stream(
self._current_group.stream
@@ -341,6 +399,9 @@ class SnapcastClientDevice(SnapcastCoordinatorEntity, MediaPlayerEntity):
@property
def media_position(self) -> int | None:
"""Position of current playing media in seconds."""
if self._current_group is None:
return None
try:
# Position is part of properties object, not metadata object
if properties := self.coordinator.server.stream(

View File

@@ -21,6 +21,17 @@
}
}
},
"exceptions": {
"join_players_no_group": {
"message": "Client {entity_id} has no group. Unable to join players."
},
"select_source_no_group": {
"message": "Client {entity_id} has no group. Unable to select source {source}."
},
"unjoin_no_group": {
"message": "Client {entity_id} has no group. Unable to unjoin player."
}
},
"services": {
"restore": {
"description": "Restores a previously taken snapshot of a media player.",

View File

@@ -118,7 +118,6 @@ class BrowsableMedia(StrEnum):
CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played"
CURRENT_USER_TOP_ARTISTS = "current_user_top_artists"
CURRENT_USER_TOP_TRACKS = "current_user_top_tracks"
NEW_RELEASES = "new_releases"
LIBRARY_MAP = {
@@ -130,7 +129,6 @@ LIBRARY_MAP = {
BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played",
BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists",
BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks",
BrowsableMedia.NEW_RELEASES.value: "New Releases",
}
CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
@@ -166,10 +164,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.TRACK,
},
BrowsableMedia.NEW_RELEASES.value: {
"parent": MediaClass.DIRECTORY,
"children": MediaClass.ALBUM,
},
MediaType.PLAYLIST: {
"parent": MediaClass.PLAYLIST,
"children": MediaClass.TRACK,
@@ -356,14 +350,11 @@ async def build_item_response( # noqa: C901
elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS:
if top_tracks := await spotify.get_top_tracks():
items = [_get_track_item_payload(track) for track in top_tracks]
elif media_content_type == BrowsableMedia.NEW_RELEASES:
if new_releases := await spotify.get_new_releases():
items = [_get_album_item_payload(album) for album in new_releases]
elif media_content_type == MediaType.PLAYLIST:
if playlist := await spotify.get_playlist(media_content_id):
title = playlist.name
image = playlist.images[0].url if playlist.images else None
for playlist_item in playlist.tracks.items:
for playlist_item in playlist.items.items:
if playlist_item.track.type is ItemType.TRACK:
if TYPE_CHECKING:
assert isinstance(playlist_item.track, Track)

View File

@@ -6,7 +6,7 @@ from collections.abc import Mapping
import logging
from typing import Any
from spotifyaio import SpotifyClient
from spotifyaio import SpotifyClient, SpotifyForbiddenError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, CONF_TOKEN
@@ -41,6 +41,9 @@ class SpotifyFlowHandler(
try:
current_user = await spotify.get_current_user()
except SpotifyForbiddenError:
self.logger.exception("User is not subscribed to Spotify")
return self.async_abort(reason="user_not_premium")
except Exception:
self.logger.exception("Error while connecting to Spotify")
return self.async_abort(reason="connection_error")

View File

@@ -11,12 +11,15 @@ from spotifyaio import (
Playlist,
SpotifyClient,
SpotifyConnectionError,
SpotifyForbiddenError,
SpotifyNotFoundError,
UserProfile,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.util import dt as dt_util
@@ -33,6 +36,11 @@ type SpotifyConfigEntry = ConfigEntry[SpotifyData]
UPDATE_INTERVAL = timedelta(seconds=30)
FREE_API_BLOGPOST = (
"https://developer.spotify.com/blog/"
"2026-02-06-update-on-developer-access-and-platform-security"
)
@dataclass
class SpotifyCoordinatorData:
@@ -78,6 +86,19 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]):
"""Set up the coordinator."""
try:
self.current_user = await self.client.get_current_user()
except SpotifyForbiddenError as err:
async_create_issue(
self.hass,
DOMAIN,
f"user_not_premium_{self.config_entry.unique_id}",
is_fixable=False,
issue_domain=DOMAIN,
severity=IssueSeverity.ERROR,
translation_key="user_not_premium",
translation_placeholders={"entry_title": self.config_entry.title},
learn_more_url=FREE_API_BLOGPOST,
)
raise ConfigEntryError("User is not subscribed to Spotify") from err
except SpotifyConnectionError as err:
raise UpdateFailed("Error communicating with Spotify API") from err

View File

@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["spotifyaio"],
"requirements": ["spotifyaio==1.0.0"]
"requirements": ["spotifyaio==2.0.2"]
}

View File

@@ -14,10 +14,10 @@ from spotifyaio import (
Item,
ItemType,
PlaybackState,
ProductType,
RepeatMode as SpotifyRepeatMode,
Track,
)
from spotifyaio.models import ProductType
from yarl import URL
from homeassistant.components.media_player import (
@@ -222,7 +222,7 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
if item.type == ItemType.EPISODE:
if TYPE_CHECKING:
assert isinstance(item, Episode)
return item.show.publisher
return item.show.name
if TYPE_CHECKING:
assert isinstance(item, Track)
@@ -230,12 +230,10 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity):
@property
@ensure_item
def media_album_name(self, item: Item) -> str: # noqa: PLR0206
def media_album_name(self, item: Item) -> str | None: # noqa: PLR0206
"""Return the media album."""
if item.type == ItemType.EPISODE:
if TYPE_CHECKING:
assert isinstance(item, Episode)
return item.show.name
return None
if TYPE_CHECKING:
assert isinstance(item, Track)

View File

@@ -12,7 +12,8 @@
"oauth_timeout": "[%key:common::config_flow::abort::oauth2_timeout%]",
"oauth_unauthorized": "[%key:common::config_flow::abort::oauth2_unauthorized%]",
"reauth_account_mismatch": "The Spotify account authenticated with does not match the account that needed re-authentication.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"user_not_premium": "The Spotify API has been changed and Developer applications created with a free account can no longer access the API. To continue using the Spotify integration, you should use an Spotify Developer application created with a Spotify Premium account, or upgrade to Spotify Premium."
},
"create_entry": {
"default": "Successfully authenticated with Spotify."
@@ -41,6 +42,12 @@
"message": "[%key:common::exceptions::oauth2_implementation_unavailable::message%]"
}
},
"issues": {
"user_not_premium": {
"description": "[%key:component::spotify::config::abort::user_not_premium%]",
"title": "Spotify integration requires a Spotify Premium account"
}
},
"system_health": {
"info": {
"api_endpoint_reachable": "Spotify API endpoint reachable"

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.3"]
"requirements": ["starlink-grpc-core==1.2.4"]
}

View File

@@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["aioswitcher"],
"quality_scale": "silver",
"requirements": ["aioswitcher==6.1.0"],
"requirements": ["aioswitcher==6.1.1"],
"single_config_entry": true
}

View File

@@ -9,5 +9,5 @@
"iot_class": "local_push",
"loggers": ["aiotedee"],
"quality_scale": "platinum",
"requirements": ["aiotedee==0.2.25"]
"requirements": ["aiotedee==0.2.27"]
}

View File

@@ -15,5 +15,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"requirements": ["teltasync==0.1.3"]
"requirements": ["teltasync==0.2.0"]
}

View File

@@ -80,6 +80,16 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
CONF_ARM_AWAY_ACTION,
CONF_ARM_CUSTOM_BYPASS_ACTION,
CONF_ARM_HOME_ACTION,
CONF_ARM_NIGHT_ACTION,
CONF_ARM_VACATION_ACTION,
CONF_DISARM_ACTION,
CONF_TRIGGER_ACTION,
)
DEFAULT_NAME = "Template Alarm Control Panel"
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
@@ -152,6 +162,7 @@ async def async_setup_entry(
StateAlarmControlPanelEntity,
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)
@@ -172,6 +183,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_ALARM_CONTROL_PANELS,
script_options=SCRIPT_FIELDS,
)

View File

@@ -36,6 +36,8 @@ _LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Template Button"
DEFAULT_OPTIMISTIC = False
SCRIPT_FIELDS = (CONF_PRESS,)
BUTTON_YAML_SCHEMA = vol.Schema(
{
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
@@ -66,6 +68,7 @@ async def async_setup_platform(
None,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -81,6 +84,7 @@ async def async_setup_entry(
async_add_entities,
StateButtonEntity,
BUTTON_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -71,6 +71,14 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
CONF_OPEN_AND_CLOSE = "open_or_close"
SCRIPT_FIELDS = (
CLOSE_ACTION,
OPEN_ACTION,
POSITION_ACTION,
STOP_ACTION,
TILT_ACTION,
)
TILT_FEATURES = (
CoverEntityFeature.OPEN_TILT
| CoverEntityFeature.CLOSE_TILT
@@ -165,6 +173,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_COVERS,
script_options=SCRIPT_FIELDS,
)
@@ -181,6 +190,7 @@ async def async_setup_entry(
StateCoverEntity,
COVER_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -87,6 +87,15 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Fan"
SCRIPT_FIELDS = (
CONF_OFF_ACTION,
CONF_ON_ACTION,
CONF_SET_DIRECTION_ACTION,
CONF_SET_OSCILLATING_ACTION,
CONF_SET_PERCENTAGE_ACTION,
CONF_SET_PRESET_MODE_ACTION,
)
FAN_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_DIRECTION): cv.template,
@@ -159,6 +168,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_FANS,
script_options=SCRIPT_FIELDS,
)
@@ -174,6 +184,7 @@ async def async_setup_entry(
async_add_entities,
StateFanEntity,
FAN_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -8,6 +8,7 @@ import logging
from typing import Any
import voluptuous as vol
from voluptuous.humanize import humanize_error
from homeassistant.components import blueprint
from homeassistant.config_entries import ConfigEntry
@@ -25,7 +26,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
from homeassistant.helpers import issue_registry as ir, template
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.entity_platform import (
@@ -34,6 +35,7 @@ from homeassistant.helpers.entity_platform import (
async_get_platforms,
)
from homeassistant.helpers.issue_registry import IssueSeverity
from homeassistant.helpers.script import async_validate_actions_config
from homeassistant.helpers.script_variables import ScriptVariables
from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
@@ -208,6 +210,21 @@ def _format_template(value: Any, field: str | None = None) -> Any:
return str(value)
def _get_config_breadcrumbs(config: ConfigType) -> str:
"""Try to coerce entity information from the config."""
breadcrumb = "Template Entity"
# Default entity id should be in most legacy configuration because
# it's created from the legacy slug. Vacuum and Lock do not have a
# slug, therefore we need to use the name or unique_id.
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
breadcrumb = default_entity_id.split(".")[-1]
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
breadcrumb = f"unique_id: {unique_id}"
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
breadcrumb = name.template
return breadcrumb
def format_migration_config(
config: ConfigType | list[ConfigType], depth: int = 0
) -> ConfigType | list[ConfigType]:
@@ -252,16 +269,7 @@ def create_legacy_template_issue(
if domain not in PLATFORMS:
return
breadcrumb = "Template Entity"
# Default entity id should be in most legacy configuration because
# it's created from the legacy slug. Vacuum and Lock do not have a
# slug, therefore we need to use the name or unique_id.
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
breadcrumb = default_entity_id.split(".")[-1]
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
breadcrumb = f"unique_id: {unique_id}"
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
breadcrumb = name.template
breadcrumb = _get_config_breadcrumbs(config)
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
@@ -296,6 +304,39 @@ def create_legacy_template_issue(
)
async def validate_template_scripts(
hass: HomeAssistant,
config: ConfigType,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Validate template scripts."""
if not script_options:
return
def _humanize(err: Exception, data: Any) -> str:
"""Humanize vol.Invalid, stringify other exceptions."""
if isinstance(err, vol.Invalid):
return humanize_error(data, err)
return str(err)
breadcrumb: str | None = None
for script_option in script_options:
if (script_config := config.pop(script_option, None)) is not None:
try:
config[script_option] = await async_validate_actions_config(
hass, script_config
)
except (vol.Invalid, HomeAssistantError) as err:
if not breadcrumb:
breadcrumb = _get_config_breadcrumbs(config)
_LOGGER.error(
"The '%s' actions for %s failed to setup: %s",
script_option,
breadcrumb,
_humanize(err, script_config),
)
async def async_setup_template_platform(
hass: HomeAssistant,
domain: str,
@@ -306,6 +347,7 @@ async def async_setup_template_platform(
discovery_info: DiscoveryInfoType | None,
legacy_fields: dict[str, str] | None = None,
legacy_key: str | None = None,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Set up the Template platform."""
if discovery_info is None:
@@ -337,10 +379,14 @@ async def async_setup_template_platform(
# Trigger Configuration
if "coordinator" in discovery_info:
if trigger_entity_cls:
entities = [
trigger_entity_cls(hass, discovery_info["coordinator"], config)
for config in discovery_info["entities"]
]
entities = []
for entity_config in discovery_info["entities"]:
await validate_template_scripts(hass, entity_config, script_options)
entities.append(
trigger_entity_cls(
hass, discovery_info["coordinator"], entity_config
)
)
async_add_entities(entities)
else:
raise PlatformNotReady(
@@ -349,6 +395,9 @@ async def async_setup_template_platform(
return
# Modern Configuration
for entity_config in discovery_info["entities"]:
await validate_template_scripts(hass, entity_config, script_options)
async_create_template_tracking_entities(
state_entity_cls,
async_add_entities,
@@ -365,6 +414,7 @@ async def async_setup_template_entry(
state_entity_cls: type[TemplateEntity],
config_schema: vol.Schema | vol.All,
replace_value_template: bool = False,
script_options: tuple[str, ...] | None = None,
) -> None:
"""Setup the Template from a config entry."""
options = dict(config_entry.options)
@@ -377,6 +427,7 @@ async def async_setup_template_entry(
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
validated_config = config_schema(options)
await validate_template_scripts(hass, validated_config, script_options)
async_add_entities(
[state_entity_cls(hass, validated_config, config_entry.entry_id)]

View File

@@ -129,6 +129,18 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Light"
SCRIPT_FIELDS = (
CONF_EFFECT_ACTION,
CONF_HS_ACTION,
CONF_LEVEL_ACTION,
CONF_OFF_ACTION,
CONF_ON_ACTION,
CONF_RGB_ACTION,
CONF_RGBW_ACTION,
CONF_RGBWW_ACTION,
CONF_TEMPERATURE_ACTION,
)
LIGHT_COMMON_SCHEMA = vol.Schema(
{
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
@@ -142,8 +154,6 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
vol.Optional(CONF_MIN_MIREDS): cv.template,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_RGB): cv.template,
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
@@ -226,6 +236,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_LIGHTS,
script_options=SCRIPT_FIELDS,
)
@@ -242,6 +253,7 @@ async def async_setup_entry(
StateLightEntity,
LIGHT_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -64,6 +64,13 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
CONF_LOCK,
CONF_OPEN,
CONF_UNLOCK,
)
LOCK_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_CODE_FORMAT): cv.template,
@@ -112,6 +119,7 @@ async def async_setup_platform(
async_add_entities,
discovery_info,
LEGACY_FIELDS,
script_options=SCRIPT_FIELDS,
)
@@ -127,6 +135,7 @@ async def async_setup_entry(
async_add_entities,
StateLockEntity,
LOCK_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -46,6 +46,8 @@ CONF_SET_VALUE = "set_value"
DEFAULT_NAME = "Template Number"
DEFAULT_OPTIMISTIC = False
SCRIPT_FIELDS = (CONF_SET_VALUE,)
NUMBER_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
@@ -81,6 +83,7 @@ async def async_setup_platform(
TriggerNumberEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -96,6 +99,7 @@ async def async_setup_entry(
async_add_entities,
StateNumberEntity,
NUMBER_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -47,6 +47,8 @@ CONF_SELECT_OPTION = "select_option"
DEFAULT_NAME = "Template Select"
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
SELECT_COMMON_SCHEMA = vol.Schema(
{
vol.Required(ATTR_OPTIONS): cv.template,
@@ -79,6 +81,7 @@ async def async_setup_platform(
TriggerSelectEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -94,6 +97,7 @@ async def async_setup_entry(
async_add_entities,
TemplateSelect,
SELECT_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -57,11 +57,16 @@ LEGACY_FIELDS = {
DEFAULT_NAME = "Template Switch"
SCRIPT_FIELDS = (
CONF_TURN_OFF,
CONF_TURN_ON,
)
SWITCH_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_STATE): cv.template,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
}
)
@@ -109,6 +114,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_SWITCHES,
script_options=SCRIPT_FIELDS,
)
@@ -125,6 +131,7 @@ async def async_setup_entry(
StateSwitchEntity,
SWITCH_CONFIG_ENTRY_SCHEMA,
True,
script_options=SCRIPT_FIELDS,
)

View File

@@ -266,13 +266,20 @@ class TemplateEntity(AbstractTemplateEntity):
def _get_this_variable(self) -> TemplateStateFromEntityId:
"""Create a this variable for the entity."""
entity_id = self.entity_id
if self._preview_callback:
preview_entity_id = async_generate_entity_id(
self._entity_id_format, self._attr_name or "preview", hass=self.hass
)
return TemplateStateFromEntityId(self.hass, preview_entity_id)
# During config flow, the registry entry and entity_id will be None. In this scenario,
# a temporary entity_id is created.
# During option flow, the preview entity_id will be None, however the registry entry
# will contain the target entity_id.
if self.registry_entry:
entity_id = self.registry_entry.entity_id
else:
entity_id = async_generate_entity_id(
self._entity_id_format, self._attr_name or "preview", hass=self.hass
)
return TemplateStateFromEntityId(self.hass, self.entity_id)
return TemplateStateFromEntityId(self.hass, entity_id)
def _render_script_variables(self) -> dict[str, Any]:
"""Render configured variables."""

View File

@@ -65,6 +65,8 @@ CONF_SPECIFIC_VERSION = "specific_version"
CONF_TITLE = "title"
CONF_UPDATE_PERCENTAGE = "update_percentage"
SCRIPT_FIELDS = (CONF_INSTALL,)
UPDATE_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
@@ -105,6 +107,7 @@ async def async_setup_platform(
TriggerUpdateEntity,
async_add_entities,
discovery_info,
script_options=SCRIPT_FIELDS,
)
@@ -120,6 +123,7 @@ async def async_setup_entry(
async_add_entities,
StateUpdateEntity,
UPDATE_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -76,6 +76,16 @@ LEGACY_FIELDS = {
CONF_VALUE_TEMPLATE: CONF_STATE,
}
SCRIPT_FIELDS = (
SERVICE_CLEAN_SPOT,
SERVICE_LOCATE,
SERVICE_PAUSE,
SERVICE_RETURN_TO_BASE,
SERVICE_SET_FAN_SPEED,
SERVICE_START,
SERVICE_STOP,
)
VACUUM_COMMON_SCHEMA = vol.Schema(
{
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
@@ -150,6 +160,7 @@ async def async_setup_platform(
discovery_info,
LEGACY_FIELDS,
legacy_key=CONF_VACUUMS,
script_options=SCRIPT_FIELDS,
)
@@ -165,6 +176,7 @@ async def async_setup_entry(
async_add_entities,
TemplateStateVacuumEntity,
VACUUM_CONFIG_ENTRY_SCHEMA,
script_options=SCRIPT_FIELDS,
)

View File

@@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/thread",
"integration_type": "service",
"iot_class": "local_polling",
"requirements": ["python-otbr-api==2.8.0", "pyroute2==0.7.5"],
"requirements": ["python-otbr-api==2.9.0", "pyroute2==0.7.5"],
"single_config_entry": true,
"zeroconf": ["_meshcop._udp.local."]
}

View File

@@ -2,9 +2,10 @@
from __future__ import annotations
from datetime import timedelta
import asyncio
from datetime import datetime, timedelta
import logging
from typing import TYPE_CHECKING, cast
from typing import TYPE_CHECKING, TypedDict, cast
from aiohttp.client_exceptions import ClientError
import tibber
@@ -38,6 +39,58 @@ FIVE_YEARS = 5 * 365 * 24
_LOGGER = logging.getLogger(__name__)
class TibberHomeData(TypedDict):
"""Data for a Tibber home used by the price sensor."""
currency: str
price_unit: str
current_price: float | None
current_price_time: datetime | None
intraday_price_ranking: float | None
max_price: float
avg_price: float
min_price: float
off_peak_1: float
peak: float
off_peak_2: float
month_cost: float | None
peak_hour: float | None
peak_hour_time: datetime | None
month_cons: float | None
app_nickname: str | None
grid_company: str | None
estimated_annual_consumption: int | None
def _build_home_data(home: tibber.TibberHome) -> TibberHomeData:
"""Build TibberHomeData from a TibberHome for the price sensor."""
current_price, last_updated, price_rank = home.current_price_data()
attributes = home.current_attributes()
result: TibberHomeData = {
"currency": home.currency,
"price_unit": home.price_unit,
"current_price": current_price,
"current_price_time": last_updated,
"intraday_price_ranking": price_rank,
"max_price": attributes["max_price"],
"avg_price": attributes["avg_price"],
"min_price": attributes["min_price"],
"off_peak_1": attributes["off_peak_1"],
"peak": attributes["peak"],
"off_peak_2": attributes["off_peak_2"],
"month_cost": home.month_cost,
"peak_hour": home.peak_hour,
"peak_hour_time": home.peak_hour_time,
"month_cons": home.month_cons,
"app_nickname": home.info["viewer"]["home"].get("appNickname"),
"grid_company": home.info["viewer"]["home"]["meteringPointData"]["gridCompany"],
"estimated_annual_consumption": home.info["viewer"]["home"][
"meteringPointData"
]["estimatedAnnualConsumption"],
}
return result
class TibberDataCoordinator(DataUpdateCoordinator[None]):
"""Handle Tibber data and insert statistics."""
@@ -57,13 +110,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
name=f"Tibber {tibber_connection.name}",
update_interval=timedelta(minutes=20),
)
self._tibber_connection = tibber_connection
async def _async_update_data(self) -> None:
"""Update data via API."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
try:
await self._tibber_connection.fetch_consumption_data_active_homes()
await self._tibber_connection.fetch_production_data_active_homes()
await tibber_connection.fetch_consumption_data_active_homes()
await tibber_connection.fetch_production_data_active_homes()
await self._insert_statistics()
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
@@ -75,7 +131,10 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async def _insert_statistics(self) -> None:
"""Insert Tibber statistics."""
for home in self._tibber_connection.get_homes():
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
for home in tibber_connection.get_homes():
sensors: list[tuple[str, bool, str | None, str]] = []
if home.hourly_consumption_data:
sensors.append(
@@ -194,6 +253,76 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
async_add_external_statistics(self.hass, metadata, statistics)
class TibberPriceCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
"""Handle Tibber price data and insert statistics."""
config_entry: TibberConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: TibberConfigEntry,
) -> None:
"""Initialize the price coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=f"{DOMAIN} price",
update_interval=timedelta(minutes=1),
)
def _seconds_until_next_15_minute(self) -> float:
"""Return seconds 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:
next_run = now.replace(minute=0, second=0, microsecond=0) + timedelta(
hours=1
)
else:
next_run = now.replace(
minute=next_minute, second=0, microsecond=0, tzinfo=dt_util.UTC
)
return (next_run - now).total_seconds()
async def _async_update_data(self) -> dict[str, TibberHomeData]:
"""Update data via API and return per-home data for sensors."""
tibber_connection = await self.config_entry.runtime_data.async_get_client(
self.hass
)
active_homes = tibber_connection.get_homes(only_active=True)
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)
)
except tibber.RetryableHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
except tibber.FatalHttpExceptionError as err:
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
result = {home.home_id: _build_home_data(home) for home in active_homes}
self.update_interval = timedelta(seconds=self._seconds_until_next_15_minute())
return result
class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
"""Fetch and cache Tibber Data API device capabilities."""

View File

@@ -3,10 +3,8 @@
from __future__ import annotations
from collections.abc import Callable
import datetime
from datetime import timedelta
import logging
from random import randrange
from typing import Any
import aiohttp
@@ -42,18 +40,20 @@ from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.util import Throttle, dt as dt_util
from homeassistant.util import dt as dt_util
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
from .coordinator import (
TibberDataAPICoordinator,
TibberDataCoordinator,
TibberPriceCoordinator,
)
_LOGGER = logging.getLogger(__name__)
ICON = "mdi:currency-usd"
SCAN_INTERVAL = timedelta(minutes=1)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
PARALLEL_UPDATES = 0
TWENTY_MINUTES = 20 * 60
RT_SENSORS_UNIQUE_ID_MIGRATION = {
"accumulated_consumption_last_hour": "accumulated consumption current hour",
@@ -610,6 +610,7 @@ async def _async_setup_graphql_sensors(
entity_registry = er.async_get(hass)
coordinator: TibberDataCoordinator | None = None
price_coordinator: TibberPriceCoordinator | None = None
entities: list[TibberSensor] = []
for home in tibber_connection.get_homes(only_active=False):
try:
@@ -626,7 +627,9 @@ async def _async_setup_graphql_sensors(
raise PlatformNotReady from err
if home.has_active_subscription:
entities.append(TibberSensorElPrice(home))
if price_coordinator is None:
price_coordinator = TibberPriceCoordinator(hass, entry)
entities.append(TibberSensorElPrice(price_coordinator, home))
if coordinator is None:
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
entities.extend(
@@ -737,19 +740,21 @@ class TibberSensor(SensorEntity):
return device_info
class TibberSensorElPrice(TibberSensor):
class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator]):
"""Representation of a Tibber sensor for el price."""
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_translation_key = "electricity_price"
def __init__(self, tibber_home: TibberHome) -> None:
def __init__(
self,
coordinator: TibberPriceCoordinator,
tibber_home: TibberHome,
) -> None:
"""Initialize the sensor."""
super().__init__(tibber_home=tibber_home)
self._last_updated: datetime.datetime | None = None
self._spread_load_constant = randrange(TWENTY_MINUTES)
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
self._attr_available = False
self._attr_native_unit_of_measurement = tibber_home.price_unit
self._attr_extra_state_attributes = {
"app_nickname": None,
"grid_company": None,
@@ -768,51 +773,38 @@ class TibberSensorElPrice(TibberSensor):
self._device_name = self._home_name
async def async_update(self) -> None:
"""Get the latest data and updates the states."""
now = dt_util.now()
if (
not self._tibber_home.last_data_timestamp
or (self._tibber_home.last_data_timestamp - now).total_seconds()
< 10 * 3600 - self._spread_load_constant
or not self.available
):
_LOGGER.debug("Asking for new data")
await self._fetch_data()
elif (
self._tibber_home.price_total
and self._last_updated
and self._last_updated.hour == now.hour
and now - self._last_updated < timedelta(minutes=15)
and self._tibber_home.last_data_timestamp
@callback
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
data = self.coordinator.data
if not data or (
(home_data := data.get(self._tibber_home.home_id)) is None
or (current_price := home_data.get("current_price")) is None
):
self._attr_available = False
self.async_write_ha_state()
return
res = self._tibber_home.current_price_data()
self._attr_native_value, self._last_updated, price_rank = res
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
attrs = self._tibber_home.current_attributes()
self._attr_extra_state_attributes.update(attrs)
self._attr_available = self._attr_native_value is not None
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def _fetch_data(self) -> None:
_LOGGER.debug("Fetching data")
try:
await self._tibber_home.update_info_and_price_info()
except TimeoutError, aiohttp.ClientError:
return
data = self._tibber_home.info["viewer"]["home"]
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
"gridCompany"
self._attr_native_unit_of_measurement = home_data.get(
"price_unit", self._tibber_home.price_unit
)
self._attr_native_value = current_price
self._attr_extra_state_attributes["intraday_price_ranking"] = home_data.get(
"intraday_price_ranking"
)
self._attr_extra_state_attributes["max_price"] = home_data["max_price"]
self._attr_extra_state_attributes["avg_price"] = home_data["avg_price"]
self._attr_extra_state_attributes["min_price"] = home_data["min_price"]
self._attr_extra_state_attributes["off_peak_1"] = home_data["off_peak_1"]
self._attr_extra_state_attributes["peak"] = home_data["peak"]
self._attr_extra_state_attributes["off_peak_2"] = home_data["off_peak_2"]
self._attr_extra_state_attributes["app_nickname"] = home_data["app_nickname"]
self._attr_extra_state_attributes["grid_company"] = home_data["grid_company"]
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
"estimated_annual_consumption"
]
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
"meteringPointData"
]["estimatedAnnualConsumption"]
self._attr_available = True
self.async_write_ha_state()
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):

View File

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

View File

@@ -81,6 +81,7 @@ clean_area:
selector:
area:
multiple: true
reorder: true
send_command:
target:

View File

@@ -398,7 +398,7 @@ SENSOR_DESCRIPTIONS = {
Keys.WARNING: VictronBLESensorEntityDescription(
key=Keys.WARNING,
device_class=SensorDeviceClass.ENUM,
translation_key="alarm",
translation_key="warning",
options=ALARM_OPTIONS,
),
Keys.YIELD_TODAY: VictronBLESensorEntityDescription(
@@ -410,7 +410,7 @@ SENSOR_DESCRIPTIONS = {
),
}
for i in range(1, 8):
for i in range(1, 9):
cell_key = getattr(Keys, f"CELL_{i}_VOLTAGE")
SENSOR_DESCRIPTIONS[cell_key] = VictronBLESensorEntityDescription(
key=cell_key,
@@ -418,6 +418,7 @@ for i in range(1, 8):
device_class=SensorDeviceClass.VOLTAGE,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
translation_placeholders={"cell": str(i)},
)

View File

@@ -248,7 +248,24 @@
"name": "[%key:component::victron_ble::common::starter_voltage%]"
},
"warning": {
"name": "Warning"
"name": "Warning",
"state": {
"bms_lockout": "[%key:component::victron_ble::entity::sensor::alarm::state::bms_lockout%]",
"dc_ripple": "[%key:component::victron_ble::entity::sensor::alarm::state::dc_ripple%]",
"high_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_starter_voltage%]",
"high_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::high_temperature%]",
"high_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::high_v_ac_out%]",
"high_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::high_voltage%]",
"low_soc": "[%key:component::victron_ble::entity::sensor::alarm::state::low_soc%]",
"low_starter_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_starter_voltage%]",
"low_temperature": "[%key:component::victron_ble::entity::sensor::alarm::state::low_temperature%]",
"low_v_ac_out": "[%key:component::victron_ble::entity::sensor::alarm::state::low_v_ac_out%]",
"low_voltage": "[%key:component::victron_ble::entity::sensor::alarm::state::low_voltage%]",
"mid_voltage": "[%key:component::victron_ble::common::midpoint_voltage%]",
"no_alarm": "[%key:component::victron_ble::entity::sensor::alarm::state::no_alarm%]",
"overload": "[%key:component::victron_ble::entity::sensor::alarm::state::overload%]",
"short_circuit": "[%key:component::victron_ble::entity::sensor::alarm::state::short_circuit%]"
}
},
"yield_today": {
"name": "Yield today"

View File

@@ -78,6 +78,7 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
data,
session,
)
self._session = session
# Last resort as no MAC or S/N can be retrieved via API
self._id = config_entry.unique_id
@@ -135,11 +136,15 @@ class VodafoneStationRouter(DataUpdateCoordinator[UpdateCoordinatorDataType]):
_LOGGER.debug("Polling Vodafone Station host: %s", self.api.base_url.host)
try:
await self.api.login()
if not self._session.cookie_jar.filter_cookies(self.api.base_url):
_LOGGER.debug(
"Session cookies missing for host %s, re-login",
self.api.base_url.host,
)
await self.api.login()
raw_data_devices = await self.api.get_devices_data()
data_sensors = await self.api.get_sensor_data()
data_wifi = await self.api.get_wifi_data()
await self.api.logout()
except exceptions.CannotAuthenticate as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,

View File

@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["aiovodafone"],
"quality_scale": "platinum",
"requirements": ["aiovodafone==3.1.2"]
"requirements": ["aiovodafone==3.1.3"]
}

View File

@@ -104,6 +104,7 @@ class VodafoneSwitchEntity(CoordinatorEntity[VodafoneStationRouter], SwitchEntit
await self.coordinator.api.set_wifi_status(
status, self.entity_description.typology, self.entity_description.band
)
await self.coordinator.async_request_refresh()
except CannotAuthenticate as err:
self.coordinator.config_entry.async_start_reauth(self.hass)
raise HomeAssistantError(

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