forked from home-assistant/core
Compare commits
105 Commits
llm-task-a
...
epenet-202
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f909df1afe | ||
|
|
73bed96a0f | ||
|
|
0a5d13f104 | ||
|
|
d16ec81727 | ||
|
|
11564e3df5 | ||
|
|
341d9f15f0 | ||
|
|
2c13c70e12 | ||
|
|
73d0d87705 | ||
|
|
b8dfb2c850 | ||
|
|
cf67a68454 | ||
|
|
b003429912 | ||
|
|
4aff032442 | ||
|
|
da3d8a6332 | ||
|
|
7a5c088149 | ||
|
|
31eec6f471 | ||
|
|
c602a0e279 | ||
|
|
513045e489 | ||
|
|
0db6520802 | ||
|
|
5bc2e271d2 | ||
|
|
77dca49c75 | ||
|
|
1baba8b880 | ||
|
|
875d81cab2 | ||
|
|
956f726ef3 | ||
|
|
fada81e1ce | ||
|
|
6a16424bb4 | ||
|
|
f90a740429 | ||
|
|
3dba7e5bd2 | ||
|
|
8d8ff011fc | ||
|
|
6befd065a1 | ||
|
|
9adf493acd | ||
|
|
a29d5fb56c | ||
|
|
bcb87cf812 | ||
|
|
d01758cea8 | ||
|
|
5487bfe1d9 | ||
|
|
fec65f40fc | ||
|
|
596951ea9f | ||
|
|
75d6b885cf | ||
|
|
3fad76dfa1 | ||
|
|
43d8a151ab | ||
|
|
07110e288d | ||
|
|
ba2aac4614 | ||
|
|
3449dae7a2 | ||
|
|
b8cd3f3635 | ||
|
|
be53ad5449 | ||
|
|
ffd940e07c | ||
|
|
5e31b5ac4f | ||
|
|
81257f9d57 | ||
|
|
ce1678719a | ||
|
|
fc6844b3c9 | ||
|
|
8e82e3aa3a | ||
|
|
3bc68941e6 | ||
|
|
e69b38ab2c | ||
|
|
ed9503324d | ||
|
|
22a06a6c2e | ||
|
|
3b611b9b03 | ||
|
|
79cc3bffc6 | ||
|
|
5c455304a5 | ||
|
|
058f860be7 | ||
|
|
ef319c966d | ||
|
|
adc4e9fdc1 | ||
|
|
40a00fb790 | ||
|
|
0926b16095 | ||
|
|
308c89af4a | ||
|
|
b0c2a47288 | ||
|
|
c446cce2cc | ||
|
|
e02267ad89 | ||
|
|
36381e6753 | ||
|
|
6533562f4e | ||
|
|
1bc6ea98ce | ||
|
|
bab34b844b | ||
|
|
ad3dac0373 | ||
|
|
c5d93e5456 | ||
|
|
ef9b46dce5 | ||
|
|
6f3ceb83c2 | ||
|
|
589577a04c | ||
|
|
cb21bb6542 | ||
|
|
ad64139b8e | ||
|
|
9ae0cfc7e5 | ||
|
|
dffaf49eca | ||
|
|
4add783108 | ||
|
|
421251308f | ||
|
|
cce878213f | ||
|
|
664441eaec | ||
|
|
d4686a3cce | ||
|
|
6e92247799 | ||
|
|
f5355c833e | ||
|
|
add9f4c5ab | ||
|
|
38973fe64a | ||
|
|
d657964729 | ||
|
|
25c408484c | ||
|
|
c335b5b37c | ||
|
|
61b00892c3 | ||
|
|
e47e2c92fe | ||
|
|
3283965b45 | ||
|
|
4a9cbc79f2 | ||
|
|
33978ce59e | ||
|
|
d5262231a1 | ||
|
|
b563f9078a | ||
|
|
e8667dfbe0 | ||
|
|
8d4f5d78ff | ||
|
|
e354a850c9 | ||
|
|
5ea026d369 | ||
|
|
ddfe17d0a4 | ||
|
|
85aa7bef1e | ||
|
|
8498928e47 |
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,15 +1,14 @@
|
||||
name: Report an issue with Home Assistant Core
|
||||
description: Report an issue with Home Assistant Core.
|
||||
type: Bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
This issue form is for reporting bugs only!
|
||||
|
||||
If you have a feature or enhancement request, please use the [feature request][fr] section of our [Community Forum][fr].
|
||||
If you have a feature or enhancement request, please [request them here instead][fr].
|
||||
|
||||
[fr]: https://community.home-assistant.io/c/feature-requests
|
||||
[fr]: https://github.com/orgs/home-assistant/discussions
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -10,8 +10,8 @@ contact_links:
|
||||
url: https://www.home-assistant.io/help
|
||||
about: We use GitHub for tracking bugs, check our website for resources on getting help.
|
||||
- name: Feature Request
|
||||
url: https://community.home-assistant.io/c/feature-requests
|
||||
about: Please use our Community Forum for making feature requests.
|
||||
url: https://github.com/orgs/home-assistant/discussions
|
||||
about: Please use this link to request new features or enhancements to existing features.
|
||||
- name: I'm unsure where to go
|
||||
url: https://www.home-assistant.io/join-chat
|
||||
about: If you are unsure where to go, then joining our chat is recommended; Just ask!
|
||||
|
||||
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@@ -94,7 +94,7 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of frontend
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/frontend
|
||||
@@ -105,10 +105,10 @@ jobs:
|
||||
|
||||
- name: Download nightly wheels of intents
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: dawidd6/action-download-artifact@v10
|
||||
uses: dawidd6/action-download-artifact@v11
|
||||
with:
|
||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
||||
repo: home-assistant/intents-package
|
||||
repo: OHF-Voice/intents-package
|
||||
branch: main
|
||||
workflow: nightly.yaml
|
||||
workflow_conclusion: success
|
||||
|
||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -37,7 +37,7 @@ on:
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
CACHE_VERSION: 2
|
||||
CACHE_VERSION: 3
|
||||
UV_CACHE_VERSION: 1
|
||||
MYPY_CACHE_VERSION: 1
|
||||
HA_SHORT_VERSION: "2025.7"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.11.12
|
||||
rev: v0.12.0
|
||||
hooks:
|
||||
- id: ruff-check
|
||||
args:
|
||||
|
||||
@@ -38,8 +38,7 @@ def validate_python() -> None:
|
||||
|
||||
def ensure_config_path(config_dir: str) -> None:
|
||||
"""Validate the configuration directory."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
lib_dir = os.path.join(config_dir, "deps")
|
||||
|
||||
@@ -80,8 +79,7 @@ def ensure_config_path(config_dir: str) -> None:
|
||||
|
||||
def get_arguments() -> argparse.Namespace:
|
||||
"""Get parsed passed in arguments."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config as config_util
|
||||
from . import config as config_util # noqa: PLC0415
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Home Assistant: Observe, Control, Automate.",
|
||||
@@ -177,8 +175,7 @@ def main() -> int:
|
||||
validate_os()
|
||||
|
||||
if args.script is not None:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import scripts
|
||||
from . import scripts # noqa: PLC0415
|
||||
|
||||
return scripts.run(args.script)
|
||||
|
||||
@@ -188,8 +185,7 @@ def main() -> int:
|
||||
|
||||
ensure_config_path(config_dir)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import config, runner
|
||||
from . import config, runner # noqa: PLC0415
|
||||
|
||||
safe_mode = config.safe_mode_enabled(config_dir)
|
||||
|
||||
|
||||
@@ -52,28 +52,28 @@ _LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def _generate_secret() -> str:
|
||||
"""Generate a secret."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return str(pyotp.random_base32())
|
||||
|
||||
|
||||
def _generate_random() -> int:
|
||||
"""Generate a 32 digit number."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return int(pyotp.random_base32(length=32, chars=list("1234567890")))
|
||||
|
||||
|
||||
def _generate_otp(secret: str, count: int) -> str:
|
||||
"""Generate one time password."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return str(pyotp.HOTP(secret).at(count))
|
||||
|
||||
|
||||
def _verify_otp(secret: str, otp: str, count: int) -> bool:
|
||||
"""Verify one time password."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
return bool(pyotp.HOTP(secret).verify(otp, count))
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ DUMMY_SECRET = "FPPTH34D4E3MI2HG"
|
||||
|
||||
def _generate_qr_code(data: str) -> str:
|
||||
"""Generate a base64 PNG string represent QR Code image of data."""
|
||||
import pyqrcode # pylint: disable=import-outside-toplevel
|
||||
import pyqrcode # noqa: PLC0415
|
||||
|
||||
qr_code = pyqrcode.create(data)
|
||||
|
||||
@@ -59,7 +59,7 @@ def _generate_qr_code(data: str) -> str:
|
||||
|
||||
def _generate_secret_and_qr_code(username: str) -> tuple[str, str, str]:
|
||||
"""Generate a secret, url, and QR code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
ota_secret = pyotp.random_base32()
|
||||
url = pyotp.totp.TOTP(ota_secret).provisioning_uri(
|
||||
@@ -107,7 +107,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
def _add_ota_secret(self, user_id: str, secret: str | None = None) -> str:
|
||||
"""Create a ota_secret for user."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
ota_secret: str = secret or pyotp.random_base32()
|
||||
|
||||
@@ -163,7 +163,7 @@ class TotpAuthModule(MultiFactorAuthModule):
|
||||
|
||||
def _validate_2fa(self, user_id: str, code: str) -> bool:
|
||||
"""Validate two factor authentication code."""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
if (ota_secret := self._users.get(user_id)) is None: # type: ignore[union-attr]
|
||||
# even we cannot find user, we still do verify
|
||||
@@ -196,7 +196,7 @@ class TotpSetupFlow(SetupFlow[TotpAuthModule]):
|
||||
Return self.async_show_form(step_id='init') if user_input is None.
|
||||
Return self.async_create_entry(data={'result': result}) if finish.
|
||||
"""
|
||||
import pyotp # pylint: disable=import-outside-toplevel
|
||||
import pyotp # noqa: PLC0415
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
|
||||
@@ -394,7 +394,7 @@ async def async_setup_hass(
|
||||
|
||||
def open_hass_ui(hass: core.HomeAssistant) -> None:
|
||||
"""Open the UI."""
|
||||
import webbrowser # pylint: disable=import-outside-toplevel
|
||||
import webbrowser # noqa: PLC0415
|
||||
|
||||
if hass.config.api is None or "frontend" not in hass.config.components:
|
||||
_LOGGER.warning("Cannot launch the UI because frontend not loaded")
|
||||
@@ -561,8 +561,7 @@ async def async_enable_logging(
|
||||
|
||||
if not log_no_color:
|
||||
try:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from colorlog import ColoredFormatter
|
||||
from colorlog import ColoredFormatter # noqa: PLC0415
|
||||
|
||||
# basicConfig must be called after importing colorlog in order to
|
||||
# ensure that the handlers it sets up wraps the correct streams.
|
||||
@@ -606,7 +605,7 @@ async def async_enable_logging(
|
||||
)
|
||||
threading.excepthook = lambda args: logging.getLogger().exception(
|
||||
"Uncaught thread exception",
|
||||
exc_info=( # type: ignore[arg-type]
|
||||
exc_info=( # type: ignore[arg-type] # noqa: LOG014
|
||||
args.exc_type,
|
||||
args.exc_value,
|
||||
args.exc_traceback,
|
||||
@@ -1060,5 +1059,5 @@ async def _async_setup_multi_components(
|
||||
_LOGGER.error(
|
||||
"Error setting up integration %s - received exception",
|
||||
domain,
|
||||
exc_info=(type(result), result, result.__traceback__),
|
||||
exc_info=(type(result), result, result.__traceback__), # noqa: LOG014
|
||||
)
|
||||
|
||||
@@ -39,14 +39,14 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
try:
|
||||
location_point_valid = await test_location(
|
||||
location_point_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
user_input["longitude"],
|
||||
)
|
||||
if not location_point_valid:
|
||||
location_nearest_valid = await test_location(
|
||||
location_nearest_valid = await check_location(
|
||||
websession,
|
||||
user_input["api_key"],
|
||||
user_input["latitude"],
|
||||
@@ -88,7 +88,7 @@ class AirlyFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
|
||||
|
||||
async def test_location(
|
||||
async def check_location(
|
||||
client: ClientSession,
|
||||
api_key: str,
|
||||
latitude: float,
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aioamazondevices==3.1.4"]
|
||||
"requirements": ["aioamazondevices==3.1.12"]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfApparentPower,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
@@ -35,6 +36,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"alarmdel": SensorEntityDescription(
|
||||
key="alarmdel",
|
||||
translation_key="alarm_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"ambtemp": SensorEntityDescription(
|
||||
key="ambtemp",
|
||||
@@ -47,15 +49,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="apc",
|
||||
translation_key="apc_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"apcmodel": SensorEntityDescription(
|
||||
key="apcmodel",
|
||||
translation_key="apc_model",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"badbatts": SensorEntityDescription(
|
||||
key="badbatts",
|
||||
translation_key="bad_batteries",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"battdate": SensorEntityDescription(
|
||||
key="battdate",
|
||||
@@ -82,6 +87,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="cable",
|
||||
translation_key="cable_type",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"cumonbatt": SensorEntityDescription(
|
||||
key="cumonbatt",
|
||||
@@ -94,52 +100,63 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="date",
|
||||
translation_key="date",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dipsw": SensorEntityDescription(
|
||||
key="dipsw",
|
||||
translation_key="dip_switch_settings",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dlowbatt": SensorEntityDescription(
|
||||
key="dlowbatt",
|
||||
translation_key="low_battery_signal",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"driver": SensorEntityDescription(
|
||||
key="driver",
|
||||
translation_key="driver",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dshutd": SensorEntityDescription(
|
||||
key="dshutd",
|
||||
translation_key="shutdown_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"dwake": SensorEntityDescription(
|
||||
key="dwake",
|
||||
translation_key="wake_delay",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"end apc": SensorEntityDescription(
|
||||
key="end apc",
|
||||
translation_key="date_and_time",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"extbatts": SensorEntityDescription(
|
||||
key="extbatts",
|
||||
translation_key="external_batteries",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"firmware": SensorEntityDescription(
|
||||
key="firmware",
|
||||
translation_key="firmware_version",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"hitrans": SensorEntityDescription(
|
||||
key="hitrans",
|
||||
translation_key="transfer_high",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"hostname": SensorEntityDescription(
|
||||
key="hostname",
|
||||
translation_key="hostname",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
@@ -163,10 +180,12 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="lastxfer",
|
||||
translation_key="last_transfer",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"linefail": SensorEntityDescription(
|
||||
key="linefail",
|
||||
translation_key="line_failure",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"linefreq": SensorEntityDescription(
|
||||
key="linefreq",
|
||||
@@ -198,15 +217,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
translation_key="transfer_low",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"mandate": SensorEntityDescription(
|
||||
key="mandate",
|
||||
translation_key="manufacture_date",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"masterupd": SensorEntityDescription(
|
||||
key="masterupd",
|
||||
translation_key="master_update",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"maxlinev": SensorEntityDescription(
|
||||
key="maxlinev",
|
||||
@@ -217,11 +239,13 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"maxtime": SensorEntityDescription(
|
||||
key="maxtime",
|
||||
translation_key="max_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"mbattchg": SensorEntityDescription(
|
||||
key="mbattchg",
|
||||
translation_key="max_battery_charge",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"minlinev": SensorEntityDescription(
|
||||
key="minlinev",
|
||||
@@ -232,41 +256,48 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"mintimel": SensorEntityDescription(
|
||||
key="mintimel",
|
||||
translation_key="min_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"model": SensorEntityDescription(
|
||||
key="model",
|
||||
translation_key="model",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nombattv": SensorEntityDescription(
|
||||
key="nombattv",
|
||||
translation_key="battery_nominal_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nominv": SensorEntityDescription(
|
||||
key="nominv",
|
||||
translation_key="nominal_input_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nomoutv": SensorEntityDescription(
|
||||
key="nomoutv",
|
||||
translation_key="nominal_output_voltage",
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nompower": SensorEntityDescription(
|
||||
key="nompower",
|
||||
translation_key="nominal_output_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"nomapnt": SensorEntityDescription(
|
||||
key="nomapnt",
|
||||
translation_key="nominal_apparent_power",
|
||||
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
|
||||
device_class=SensorDeviceClass.APPARENT_POWER,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"numxfers": SensorEntityDescription(
|
||||
key="numxfers",
|
||||
@@ -291,21 +322,25 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="reg1",
|
||||
translation_key="register_1_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"reg2": SensorEntityDescription(
|
||||
key="reg2",
|
||||
translation_key="register_2_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"reg3": SensorEntityDescription(
|
||||
key="reg3",
|
||||
translation_key="register_3_fault",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"retpct": SensorEntityDescription(
|
||||
key="retpct",
|
||||
translation_key="restore_capacity",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"selftest": SensorEntityDescription(
|
||||
key="selftest",
|
||||
@@ -315,20 +350,24 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="sense",
|
||||
translation_key="sensitivity",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"serialno": SensorEntityDescription(
|
||||
key="serialno",
|
||||
translation_key="serial_number",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"starttime": SensorEntityDescription(
|
||||
key="starttime",
|
||||
translation_key="startup_time",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"statflag": SensorEntityDescription(
|
||||
key="statflag",
|
||||
translation_key="online_status",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"status": SensorEntityDescription(
|
||||
key="status",
|
||||
@@ -337,6 +376,7 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
"stesti": SensorEntityDescription(
|
||||
key="stesti",
|
||||
translation_key="self_test_interval",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"timeleft": SensorEntityDescription(
|
||||
key="timeleft",
|
||||
@@ -360,23 +400,28 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="upsname",
|
||||
translation_key="ups_name",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"version": SensorEntityDescription(
|
||||
key="version",
|
||||
translation_key="version",
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbat": SensorEntityDescription(
|
||||
key="xoffbat",
|
||||
translation_key="transfer_from_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xoffbatt": SensorEntityDescription(
|
||||
key="xoffbatt",
|
||||
translation_key="transfer_from_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
"xonbatt": SensorEntityDescription(
|
||||
key="xonbatt",
|
||||
translation_key="transfer_to_battery",
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
"""Base class for assist satellite entities."""
|
||||
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from hassil.util import (
|
||||
PUNCTUATION_END,
|
||||
PUNCTUATION_END_WORD,
|
||||
PUNCTUATION_START,
|
||||
PUNCTUATION_START_WORD,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, SupportsResponse
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -23,6 +33,7 @@ from .const import (
|
||||
)
|
||||
from .entity import (
|
||||
AssistSatelliteAnnouncement,
|
||||
AssistSatelliteAnswer,
|
||||
AssistSatelliteConfiguration,
|
||||
AssistSatelliteEntity,
|
||||
AssistSatelliteEntityDescription,
|
||||
@@ -34,6 +45,7 @@ from .websocket_api import async_register_websocket_api
|
||||
__all__ = [
|
||||
"DOMAIN",
|
||||
"AssistSatelliteAnnouncement",
|
||||
"AssistSatelliteAnswer",
|
||||
"AssistSatelliteConfiguration",
|
||||
"AssistSatelliteEntity",
|
||||
"AssistSatelliteEntityDescription",
|
||||
@@ -86,6 +98,62 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"async_internal_start_conversation",
|
||||
[AssistSatelliteEntityFeature.START_CONVERSATION],
|
||||
)
|
||||
|
||||
async def handle_ask_question(call: ServiceCall) -> dict[str, Any]:
|
||||
"""Handle a Show View service call."""
|
||||
satellite_entity_id: str = call.data[ATTR_ENTITY_ID]
|
||||
satellite_entity: AssistSatelliteEntity | None = component.get_entity(
|
||||
satellite_entity_id
|
||||
)
|
||||
if satellite_entity is None:
|
||||
raise HomeAssistantError(
|
||||
f"Invalid Assist satellite entity id: {satellite_entity_id}"
|
||||
)
|
||||
|
||||
ask_question_args = {
|
||||
"question": call.data.get("question"),
|
||||
"question_media_id": call.data.get("question_media_id"),
|
||||
"preannounce": call.data.get("preannounce", False),
|
||||
"answers": call.data.get("answers"),
|
||||
}
|
||||
|
||||
if preannounce_media_id := call.data.get("preannounce_media_id"):
|
||||
ask_question_args["preannounce_media_id"] = preannounce_media_id
|
||||
|
||||
answer = await satellite_entity.async_internal_ask_question(**ask_question_args)
|
||||
|
||||
if answer is None:
|
||||
raise HomeAssistantError("No answer from satellite")
|
||||
|
||||
return asdict(answer)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service="ask_question",
|
||||
service_func=handle_ask_question,
|
||||
schema=vol.All(
|
||||
{
|
||||
vol.Required(ATTR_ENTITY_ID): cv.entity_id,
|
||||
vol.Optional("question"): str,
|
||||
vol.Optional("question_media_id"): str,
|
||||
vol.Optional("preannounce"): bool,
|
||||
vol.Optional("preannounce_media_id"): str,
|
||||
vol.Optional("answers"): [
|
||||
{
|
||||
vol.Required("id"): str,
|
||||
vol.Required("sentences"): vol.All(
|
||||
cv.ensure_list,
|
||||
[cv.string],
|
||||
has_one_non_empty_item,
|
||||
has_no_punctuation,
|
||||
),
|
||||
}
|
||||
],
|
||||
},
|
||||
cv.has_at_least_one_key("question", "question_media_id"),
|
||||
),
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
hass.data[CONNECTION_TEST_DATA] = {}
|
||||
async_register_websocket_api(hass)
|
||||
hass.http.register_view(ConnectionTestView())
|
||||
@@ -110,3 +178,29 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
|
||||
|
||||
|
||||
def has_no_punctuation(value: list[str]) -> list[str]:
|
||||
"""Validate result does not contain punctuation."""
|
||||
for sentence in value:
|
||||
if (
|
||||
PUNCTUATION_START.search(sentence)
|
||||
or PUNCTUATION_END.search(sentence)
|
||||
or PUNCTUATION_START_WORD.search(sentence)
|
||||
or PUNCTUATION_END_WORD.search(sentence)
|
||||
):
|
||||
raise vol.Invalid("sentence should not contain punctuation")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def has_one_non_empty_item(value: list[str]) -> list[str]:
|
||||
"""Validate result has at least one item."""
|
||||
if len(value) < 1:
|
||||
raise vol.Invalid("at least one sentence is required")
|
||||
|
||||
for sentence in value:
|
||||
if not sentence:
|
||||
raise vol.Invalid("sentences cannot be empty")
|
||||
|
||||
return value
|
||||
|
||||
@@ -4,12 +4,16 @@ from abc import abstractmethod
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterable
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
import time
|
||||
from typing import Any, Literal, final
|
||||
|
||||
from hassil import Intents, recognize
|
||||
from hassil.expression import Expression, ListReference, Sequence
|
||||
from hassil.intents import WildcardSlotList
|
||||
|
||||
from homeassistant.components import conversation, media_source, stt, tts
|
||||
from homeassistant.components.assist_pipeline import (
|
||||
OPTION_PREFERRED,
|
||||
@@ -105,6 +109,20 @@ class AssistSatelliteAnnouncement:
|
||||
"""Media ID to be played before announcement."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class AssistSatelliteAnswer:
|
||||
"""Answer to a question."""
|
||||
|
||||
id: str | None
|
||||
"""Matched answer id or None if no answer was matched."""
|
||||
|
||||
sentence: str
|
||||
"""Raw sentence text from user response."""
|
||||
|
||||
slots: dict[str, Any] = field(default_factory=dict)
|
||||
"""Matched slots from answer."""
|
||||
|
||||
|
||||
class AssistSatelliteEntity(entity.Entity):
|
||||
"""Entity encapsulating the state and functionality of an Assist satellite."""
|
||||
|
||||
@@ -120,8 +138,10 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
_is_announcing = False
|
||||
_extra_system_prompt: str | None = None
|
||||
_wake_word_intercept_future: asyncio.Future[str | None] | None = None
|
||||
_stt_intercept_future: asyncio.Future[str | None] | None = None
|
||||
_attr_tts_options: dict[str, Any] | None = None
|
||||
_pipeline_task: asyncio.Task | None = None
|
||||
_ask_question_future: asyncio.Future[str | None] | None = None
|
||||
|
||||
__assist_satellite_state = AssistSatelliteState.IDLE
|
||||
|
||||
@@ -309,6 +329,112 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
"""Start a conversation from the satellite."""
|
||||
raise NotImplementedError
|
||||
|
||||
async def async_internal_ask_question(
|
||||
self,
|
||||
question: str | None = None,
|
||||
question_media_id: str | None = None,
|
||||
preannounce: bool = True,
|
||||
preannounce_media_id: str = PREANNOUNCE_URL,
|
||||
answers: list[dict[str, Any]] | None = None,
|
||||
) -> AssistSatelliteAnswer | None:
|
||||
"""Ask a question and get a user's response from the satellite.
|
||||
|
||||
If question_media_id is not provided, question is synthesized to audio
|
||||
with the selected pipeline.
|
||||
|
||||
If question_media_id is provided, it is played directly. It is possible
|
||||
to omit the message and the satellite will not show any text.
|
||||
|
||||
If preannounce is True, a sound is played before the start message or media.
|
||||
If preannounce_media_id is provided, it overrides the default sound.
|
||||
|
||||
Calls async_start_conversation.
|
||||
"""
|
||||
await self._cancel_running_pipeline()
|
||||
|
||||
if question is None:
|
||||
question = ""
|
||||
|
||||
announcement = await self._resolve_announcement_media_id(
|
||||
question,
|
||||
question_media_id,
|
||||
preannounce_media_id=preannounce_media_id if preannounce else None,
|
||||
)
|
||||
|
||||
if self._is_announcing:
|
||||
raise SatelliteBusyError
|
||||
|
||||
self._is_announcing = True
|
||||
self._set_state(AssistSatelliteState.RESPONDING)
|
||||
self._ask_question_future = asyncio.Future()
|
||||
|
||||
try:
|
||||
# Wait for announcement to finish
|
||||
await self.async_start_conversation(announcement)
|
||||
|
||||
# Wait for response text
|
||||
response_text = await self._ask_question_future
|
||||
if response_text is None:
|
||||
raise HomeAssistantError("No answer from question")
|
||||
|
||||
if not answers:
|
||||
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||
|
||||
return self._question_response_to_answer(response_text, answers)
|
||||
finally:
|
||||
self._is_announcing = False
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
self._ask_question_future = None
|
||||
|
||||
def _question_response_to_answer(
|
||||
self, response_text: str, answers: list[dict[str, Any]]
|
||||
) -> AssistSatelliteAnswer:
|
||||
"""Match text to a pre-defined set of answers."""
|
||||
|
||||
# Build intents and match
|
||||
intents = Intents.from_dict(
|
||||
{
|
||||
"language": self.hass.config.language,
|
||||
"intents": {
|
||||
"QuestionIntent": {
|
||||
"data": [
|
||||
{
|
||||
"sentences": answer["sentences"],
|
||||
"metadata": {"answer_id": answer["id"]},
|
||||
}
|
||||
for answer in answers
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# Assume slot list references are wildcards
|
||||
wildcard_names: set[str] = set()
|
||||
for intent in intents.intents.values():
|
||||
for intent_data in intent.data:
|
||||
for sentence in intent_data.sentences:
|
||||
_collect_list_references(sentence, wildcard_names)
|
||||
|
||||
for wildcard_name in wildcard_names:
|
||||
intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name)
|
||||
|
||||
# Match response text
|
||||
result = recognize(response_text, intents)
|
||||
if result is None:
|
||||
# No match
|
||||
return AssistSatelliteAnswer(id=None, sentence=response_text)
|
||||
|
||||
assert result.intent_metadata
|
||||
return AssistSatelliteAnswer(
|
||||
id=result.intent_metadata["answer_id"],
|
||||
sentence=response_text,
|
||||
slots={
|
||||
entity_name: entity.value
|
||||
for entity_name, entity in result.entities.items()
|
||||
},
|
||||
)
|
||||
|
||||
async def async_accept_pipeline_from_satellite(
|
||||
self,
|
||||
audio_stream: AsyncIterable[bytes],
|
||||
@@ -351,6 +477,11 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self._internal_on_pipeline_event(PipelineEvent(PipelineEventType.RUN_END))
|
||||
return
|
||||
|
||||
if (self._ask_question_future is not None) and (
|
||||
start_stage == PipelineStage.STT
|
||||
):
|
||||
end_stage = PipelineStage.STT
|
||||
|
||||
device_id = self.registry_entry.device_id if self.registry_entry else None
|
||||
|
||||
# Refresh context if necessary
|
||||
@@ -433,6 +564,16 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
elif event.type is PipelineEventType.STT_START:
|
||||
self._set_state(AssistSatelliteState.LISTENING)
|
||||
elif event.type is PipelineEventType.STT_END:
|
||||
# Intercepting text for ask question
|
||||
if (
|
||||
(self._ask_question_future is not None)
|
||||
and (not self._ask_question_future.done())
|
||||
and event.data
|
||||
):
|
||||
self._ask_question_future.set_result(
|
||||
event.data.get("stt_output", {}).get("text")
|
||||
)
|
||||
elif event.type is PipelineEventType.INTENT_START:
|
||||
self._set_state(AssistSatelliteState.PROCESSING)
|
||||
elif event.type is PipelineEventType.TTS_START:
|
||||
@@ -443,6 +584,12 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
if not self._run_has_tts:
|
||||
self._set_state(AssistSatelliteState.IDLE)
|
||||
|
||||
if (self._ask_question_future is not None) and (
|
||||
not self._ask_question_future.done()
|
||||
):
|
||||
# No text for ask question
|
||||
self._ask_question_future.set_result(None)
|
||||
|
||||
self.on_pipeline_event(event)
|
||||
|
||||
@callback
|
||||
@@ -577,3 +724,15 @@ class AssistSatelliteEntity(entity.Entity):
|
||||
media_id_source=media_id_source,
|
||||
preannounce_media_id=preannounce_media_id,
|
||||
)
|
||||
|
||||
|
||||
def _collect_list_references(expression: Expression, list_names: set[str]) -> None:
|
||||
"""Collect list reference names recursively."""
|
||||
if isinstance(expression, Sequence):
|
||||
seq: Sequence = expression
|
||||
for item in seq.items:
|
||||
_collect_list_references(item, list_names)
|
||||
elif isinstance(expression, ListReference):
|
||||
# {list}
|
||||
list_ref: ListReference = expression
|
||||
list_names.add(list_ref.slot_name)
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
},
|
||||
"start_conversation": {
|
||||
"service": "mdi:forum"
|
||||
},
|
||||
"ask_question": {
|
||||
"service": "mdi:microphone-question"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"dependencies": ["assist_pipeline", "http", "stt", "tts"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/assist_satellite",
|
||||
"integration_type": "entity",
|
||||
"quality_scale": "internal"
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -54,3 +54,35 @@ start_conversation:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
ask_question:
|
||||
fields:
|
||||
entity_id:
|
||||
required: true
|
||||
selector:
|
||||
entity:
|
||||
domain: assist_satellite
|
||||
supported_features:
|
||||
- assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION
|
||||
question:
|
||||
required: false
|
||||
example: "What kind of music would you like to play?"
|
||||
default: ""
|
||||
selector:
|
||||
text:
|
||||
question_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
preannounce:
|
||||
required: false
|
||||
default: true
|
||||
selector:
|
||||
boolean:
|
||||
preannounce_media_id:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
answers:
|
||||
required: false
|
||||
selector:
|
||||
object:
|
||||
|
||||
@@ -59,6 +59,36 @@
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
}
|
||||
}
|
||||
},
|
||||
"ask_question": {
|
||||
"name": "Ask question",
|
||||
"description": "Asks a question and gets the user's response.",
|
||||
"fields": {
|
||||
"entity_id": {
|
||||
"name": "Entity",
|
||||
"description": "Assist satellite entity to ask the question on."
|
||||
},
|
||||
"question": {
|
||||
"name": "Question",
|
||||
"description": "The question to ask."
|
||||
},
|
||||
"question_media_id": {
|
||||
"name": "Question media ID",
|
||||
"description": "The media ID of the question to use instead of text-to-speech."
|
||||
},
|
||||
"preannounce": {
|
||||
"name": "Preannounce",
|
||||
"description": "Play a sound before the start message or media."
|
||||
},
|
||||
"preannounce_media_id": {
|
||||
"name": "Preannounce media ID",
|
||||
"description": "Custom media ID to play before the start message or media."
|
||||
},
|
||||
"answers": {
|
||||
"name": "Answers",
|
||||
"description": "Possible answers to the question."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ DATA_BLUEPRINTS = "automation_blueprints"
|
||||
|
||||
def _blueprint_in_use(hass: HomeAssistant, blueprint_path: str) -> bool:
|
||||
"""Return True if any automation references the blueprint."""
|
||||
from . import automations_with_blueprint # pylint: disable=import-outside-toplevel
|
||||
from . import automations_with_blueprint # noqa: PLC0415
|
||||
|
||||
return len(automations_with_blueprint(hass, blueprint_path)) > 0
|
||||
|
||||
@@ -28,8 +28,7 @@ async def _reload_blueprint_automations(
|
||||
@callback
|
||||
def async_get_blueprints(hass: HomeAssistant) -> blueprint.DomainBlueprints:
|
||||
"""Get automation blueprints."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA
|
||||
from .config import AUTOMATION_BLUEPRINT_SCHEMA # noqa: PLC0415
|
||||
|
||||
return blueprint.DomainBlueprints(
|
||||
hass,
|
||||
|
||||
@@ -94,8 +94,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
if not with_hassio:
|
||||
reader_writer = CoreBackupReaderWriter(hass)
|
||||
else:
|
||||
# pylint: disable-next=import-outside-toplevel, hass-component-root-import
|
||||
from homeassistant.components.hassio.backup import SupervisorBackupReaderWriter
|
||||
# pylint: disable-next=hass-component-root-import
|
||||
from homeassistant.components.hassio.backup import ( # noqa: PLC0415
|
||||
SupervisorBackupReaderWriter,
|
||||
)
|
||||
|
||||
reader_writer = SupervisorBackupReaderWriter(hass)
|
||||
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bthome",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["bthome-ble==3.12.4"]
|
||||
"requirements": ["bthome-ble==3.13.1"]
|
||||
}
|
||||
|
||||
@@ -105,11 +105,6 @@ DEFAULT_MAX_HUMIDITY = 99
|
||||
|
||||
CONVERTIBLE_ATTRIBUTE = [ATTR_TEMPERATURE, ATTR_TARGET_TEMP_LOW, ATTR_TARGET_TEMP_HIGH]
|
||||
|
||||
# Can be removed in 2025.1 after deprecation period of the new feature flags
|
||||
CHECK_TURN_ON_OFF_FEATURE_FLAG = (
|
||||
ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF
|
||||
)
|
||||
|
||||
SET_TEMPERATURE_SCHEMA = vol.All(
|
||||
cv.has_at_least_one_key(
|
||||
ATTR_TEMPERATURE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.101.0"],
|
||||
"requirements": ["hass-nabucasa==0.102.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -54,10 +54,10 @@ class Control4RuntimeData:
|
||||
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
|
||||
|
||||
|
||||
async def call_c4_api_retry(func, *func_args):
|
||||
async def call_c4_api_retry(func, *func_args): # noqa: RET503
|
||||
"""Call C4 API function and retry on failure."""
|
||||
# Ruff doesn't understand this loop - the exception is always raised after the retries
|
||||
for i in range(API_RETRY_TIMES): # noqa: RET503
|
||||
for i in range(API_RETRY_TIMES):
|
||||
try:
|
||||
return await func(*func_args)
|
||||
except client_exceptions.ClientError as exception:
|
||||
|
||||
@@ -271,7 +271,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
|
||||
# Temporary migration. We can remove this in 2024.10
|
||||
from homeassistant.components.assist_pipeline import ( # pylint: disable=import-outside-toplevel
|
||||
from homeassistant.components.assist_pipeline import ( # noqa: PLC0415
|
||||
async_migrate_engine,
|
||||
)
|
||||
|
||||
|
||||
@@ -300,10 +300,6 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def supported_features(self) -> CoverEntityFeature:
|
||||
"""Flag supported features."""
|
||||
if (features := self._attr_supported_features) is not None:
|
||||
if type(features) is int:
|
||||
new_features = CoverEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
supported_features = (
|
||||
|
||||
@@ -91,7 +91,9 @@ async def async_unload_entry(
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
|
||||
hass: HomeAssistant,
|
||||
config_entry: DevoloHomeControlConfigEntry,
|
||||
device_entry: DeviceEntry,
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
return True
|
||||
|
||||
@@ -87,6 +87,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
|
||||
):
|
||||
"""Representation of a devolo device tracker."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "device_tracker"
|
||||
|
||||
def __init__(
|
||||
@@ -99,6 +100,7 @@ class DevoloScannerEntity( # pylint: disable=hass-enforce-class-module
|
||||
super().__init__(coordinator)
|
||||
self._device = device
|
||||
self._attr_mac_address = mac
|
||||
self._attr_name = mac
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str]:
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["discord"],
|
||||
"requirements": ["nextcord==2.6.0"]
|
||||
"requirements": ["nextcord==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -108,8 +108,7 @@ def download_file(service: ServiceCall) -> None:
|
||||
_LOGGER.debug("%s -> %s", url, final_path)
|
||||
|
||||
with open(final_path, "wb") as fil:
|
||||
for chunk in req.iter_content(1024):
|
||||
fil.write(chunk)
|
||||
fil.writelines(req.iter_content(1024))
|
||||
|
||||
_LOGGER.debug("Downloading of %s done", url)
|
||||
service.hass.bus.fire(
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sml"],
|
||||
"requirements": ["pysml==0.0.12"]
|
||||
"requirements": ["pysml==0.1.5"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -30,16 +30,16 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalNumberDescription(NumberEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalNumberDescription[_DeviceT: EheimDigitalDevice](
|
||||
NumberEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital sensor entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], float | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, float], Awaitable[None]]
|
||||
uom_fn: Callable[[_DeviceT_co], str] | None = None
|
||||
value_fn: Callable[[_DeviceT], float | None]
|
||||
set_value_fn: Callable[[_DeviceT, float], Awaitable[None]]
|
||||
uom_fn: Callable[[_DeviceT], str] | None = None
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -136,7 +136,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the number entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalNumber[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalNumber[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -163,18 +163,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalNumber(
|
||||
EheimDigitalEntity[_DeviceT_co], NumberEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalNumber[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], NumberEntity
|
||||
):
|
||||
"""Represent a EHEIM Digital number entity."""
|
||||
|
||||
entity_description: EheimDigitalNumberDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalNumberDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalNumberDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalNumberDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital number entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -17,15 +17,15 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSelectDescription(SelectEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalSelectDescription[_DeviceT: EheimDigitalDevice](
|
||||
SelectEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital select entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], str | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, str], Awaitable[None]]
|
||||
value_fn: Callable[[_DeviceT], str | None]
|
||||
set_value_fn: Callable[[_DeviceT, str], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -59,7 +59,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the number entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSelect[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalSelect[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -75,18 +75,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSelect(
|
||||
EheimDigitalEntity[_DeviceT_co], SelectEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalSelect[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], SelectEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital select entity."""
|
||||
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalSelectDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSelectDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalSelectDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital select entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic, TypeVar, override
|
||||
from typing import Any, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -20,14 +20,14 @@ from .entity import EheimDigitalEntity
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalSensorDescription(SensorEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalSensorDescription[_DeviceT: EheimDigitalDevice](
|
||||
SensorEntityDescription
|
||||
):
|
||||
"""Class describing EHEIM Digital sensor entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], float | str | None]
|
||||
value_fn: Callable[[_DeviceT], float | str | None]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the light entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalSensor[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalSensor[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities += [
|
||||
@@ -91,18 +91,18 @@ async def async_setup_entry(
|
||||
async_setup_device_entities(coordinator.hub.devices)
|
||||
|
||||
|
||||
class EheimDigitalSensor(
|
||||
EheimDigitalEntity[_DeviceT_co], SensorEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalSensor[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], SensorEntity
|
||||
):
|
||||
"""Represent a EHEIM Digital sensor entity."""
|
||||
|
||||
entity_description: EheimDigitalSensorDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalSensorDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalSensorDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalSensorDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital number entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import time
|
||||
from typing import Generic, TypeVar, final, override
|
||||
from typing import Any, final, override
|
||||
|
||||
from eheimdigital.classic_vario import EheimDigitalClassicVario
|
||||
from eheimdigital.device import EheimDigitalDevice
|
||||
@@ -19,15 +19,13 @@ from .entity import EheimDigitalEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
_DeviceT_co = TypeVar("_DeviceT_co", bound=EheimDigitalDevice, covariant=True)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class EheimDigitalTimeDescription(TimeEntityDescription, Generic[_DeviceT_co]):
|
||||
class EheimDigitalTimeDescription[_DeviceT: EheimDigitalDevice](TimeEntityDescription):
|
||||
"""Class describing EHEIM Digital time entities."""
|
||||
|
||||
value_fn: Callable[[_DeviceT_co], time | None]
|
||||
set_value_fn: Callable[[_DeviceT_co, time], Awaitable[None]]
|
||||
value_fn: Callable[[_DeviceT], time | None]
|
||||
set_value_fn: Callable[[_DeviceT, time], Awaitable[None]]
|
||||
|
||||
|
||||
CLASSICVARIO_DESCRIPTIONS: tuple[
|
||||
@@ -79,7 +77,7 @@ async def async_setup_entry(
|
||||
device_address: dict[str, EheimDigitalDevice],
|
||||
) -> None:
|
||||
"""Set up the time entities for one or multiple devices."""
|
||||
entities: list[EheimDigitalTime[EheimDigitalDevice]] = []
|
||||
entities: list[EheimDigitalTime[Any]] = []
|
||||
for device in device_address.values():
|
||||
if isinstance(device, EheimDigitalClassicVario):
|
||||
entities.extend(
|
||||
@@ -103,18 +101,18 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
@final
|
||||
class EheimDigitalTime(
|
||||
EheimDigitalEntity[_DeviceT_co], TimeEntity, Generic[_DeviceT_co]
|
||||
class EheimDigitalTime[_DeviceT: EheimDigitalDevice](
|
||||
EheimDigitalEntity[_DeviceT], TimeEntity
|
||||
):
|
||||
"""Represent an EHEIM Digital time entity."""
|
||||
|
||||
entity_description: EheimDigitalTimeDescription[_DeviceT_co]
|
||||
entity_description: EheimDigitalTimeDescription[_DeviceT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EheimDigitalUpdateCoordinator,
|
||||
device: _DeviceT_co,
|
||||
description: EheimDigitalTimeDescription[_DeviceT_co],
|
||||
device: _DeviceT,
|
||||
description: EheimDigitalTimeDescription[_DeviceT],
|
||||
) -> None:
|
||||
"""Initialize an EHEIM Digital time entity."""
|
||||
super().__init__(coordinator, device)
|
||||
|
||||
@@ -60,6 +60,7 @@ _VOICE_ASSISTANT_EVENT_TYPES: EsphomeEnumMapper[
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_START: PipelineEventType.STT_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_STT_END: PipelineEventType.STT_END,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_START: PipelineEventType.INTENT_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS: PipelineEventType.INTENT_PROGRESS,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END: PipelineEventType.INTENT_END,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_START: PipelineEventType.TTS_START,
|
||||
VoiceAssistantEventType.VOICE_ASSISTANT_TTS_END: PipelineEventType.TTS_END,
|
||||
@@ -282,6 +283,12 @@ class EsphomeAssistSatellite(
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_STT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {"text": event.data["stt_output"]["text"]}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_PROGRESS:
|
||||
data_to_send = {
|
||||
"tts_start_streaming": bool(
|
||||
event.data and event.data.get("tts_start_streaming")
|
||||
),
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_INTENT_END:
|
||||
assert event.data is not None
|
||||
data_to_send = {
|
||||
@@ -332,7 +339,7 @@ class EsphomeAssistSatellite(
|
||||
}
|
||||
elif event_type == VoiceAssistantEventType.VOICE_ASSISTANT_RUN_START:
|
||||
assert event.data is not None
|
||||
if tts_output := event.data["tts_output"]:
|
||||
if tts_output := event.data.get("tts_output"):
|
||||
path = tts_output["url"]
|
||||
url = async_process_play_media_url(self.hass, path)
|
||||
data_to_send = {"url": url}
|
||||
|
||||
@@ -63,9 +63,7 @@ class ESPHomeDashboardManager:
|
||||
if not (data := self._data) or not (info := data.get("info")):
|
||||
return
|
||||
if is_hassio(self._hass):
|
||||
from homeassistant.components.hassio import ( # pylint: disable=import-outside-toplevel
|
||||
get_addons_info,
|
||||
)
|
||||
from homeassistant.components.hassio import get_addons_info # noqa: PLC0415
|
||||
|
||||
if (addons := get_addons_info(self._hass)) is not None and info[
|
||||
"addon_slug"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==32.2.1",
|
||||
"aioesphomeapi==32.2.4",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==2.16.0"
|
||||
],
|
||||
|
||||
@@ -364,8 +364,7 @@ def _frontend_root(dev_repo_path: str | None) -> pathlib.Path:
|
||||
if dev_repo_path is not None:
|
||||
return pathlib.Path(dev_repo_path) / "hass_frontend"
|
||||
# Keep import here so that we can import frontend without installing reqs
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import hass_frontend
|
||||
import hass_frontend # noqa: PLC0415
|
||||
|
||||
return hass_frontend.where()
|
||||
|
||||
|
||||
@@ -109,6 +109,7 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="timestamp",
|
||||
translation_key="timestamp",
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/google",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"],
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.0"]
|
||||
"requirements": ["gcal-sync==7.1.0", "oauth2client==4.1.3", "ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -212,8 +212,7 @@ class AbstractConfig(ABC):
|
||||
def async_enable_report_state(self) -> None:
|
||||
"""Enable proactive mode."""
|
||||
# Circular dep
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .report_state import async_enable_report_state
|
||||
from .report_state import async_enable_report_state # noqa: PLC0415
|
||||
|
||||
if self._unsub_report_state is None:
|
||||
self._unsub_report_state = async_enable_report_state(self.hass, self)
|
||||
@@ -395,8 +394,7 @@ class AbstractConfig(ABC):
|
||||
async def _handle_local_webhook(self, hass, webhook_id, request):
|
||||
"""Handle an incoming local SDK message."""
|
||||
# Circular dep
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import smart_home
|
||||
from . import smart_home # noqa: PLC0415
|
||||
|
||||
self._local_last_active = utcnow()
|
||||
|
||||
@@ -655,8 +653,9 @@ class GoogleEntity:
|
||||
if "matter" in self.hass.config.components and any(
|
||||
x for x in device_entry.identifiers if x[0] == "matter"
|
||||
):
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.matter import get_matter_device_info
|
||||
from homeassistant.components.matter import ( # noqa: PLC0415
|
||||
get_matter_device_info,
|
||||
)
|
||||
|
||||
# Import matter can block the event loop for multiple seconds
|
||||
# so we import it here to avoid blocking the event loop during
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -29,8 +29,7 @@ async def update_addon(
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_addon_before_update
|
||||
from .backup import backup_addon_before_update # noqa: PLC0415
|
||||
|
||||
await backup_addon_before_update(hass, addon, addon_name, installed_version)
|
||||
|
||||
@@ -50,8 +49,7 @@ async def update_core(hass: HomeAssistant, version: str | None, backup: bool) ->
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_core_before_update
|
||||
from .backup import backup_core_before_update # noqa: PLC0415
|
||||
|
||||
await backup_core_before_update(hass)
|
||||
|
||||
@@ -71,8 +69,7 @@ async def update_os(hass: HomeAssistant, version: str | None, backup: bool) -> N
|
||||
client = get_supervisor_client(hass)
|
||||
|
||||
if backup:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .backup import backup_core_before_update
|
||||
from .backup import backup_core_before_update # noqa: PLC0415
|
||||
|
||||
await backup_core_before_update(hass)
|
||||
|
||||
|
||||
@@ -133,8 +133,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
|
||||
"""Parse the routing response dict to a HERETravelTimeData."""
|
||||
distance: float = 0.0
|
||||
duration: float = 0.0
|
||||
duration_in_traffic: float = 0.0
|
||||
duration: int = 0
|
||||
duration_in_traffic: int = 0
|
||||
|
||||
for section in response["routes"][0]["sections"]:
|
||||
distance += DistanceConverter.convert(
|
||||
@@ -167,8 +167,8 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
destination_name = names[0]["value"]
|
||||
return HERETravelTimeData(
|
||||
attribution=None,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration_in_traffic / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration_in_traffic,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
@@ -271,13 +271,13 @@ class HERETransitDataUpdateCoordinator(
|
||||
UnitOfLength.METERS,
|
||||
UnitOfLength.KILOMETERS,
|
||||
)
|
||||
duration: float = sum(
|
||||
duration: int = sum(
|
||||
section["travelSummary"]["duration"] for section in sections
|
||||
)
|
||||
return HERETravelTimeData(
|
||||
attribution=attribution,
|
||||
duration=round(duration / 60),
|
||||
duration_in_traffic=round(duration / 60),
|
||||
duration=duration,
|
||||
duration_in_traffic=duration,
|
||||
distance=distance,
|
||||
origin=f"{mapped_origin_lat},{mapped_origin_lon}",
|
||||
destination=f"{mapped_destination_lat},{mapped_destination_lon}",
|
||||
|
||||
@@ -55,14 +55,18 @@ def sensor_descriptions(travel_mode: str) -> tuple[SensorEntityDescription, ...]
|
||||
icon=ICONS.get(travel_mode, ICON_CAR),
|
||||
key=ATTR_DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="duration_in_traffic",
|
||||
icon=ICONS.get(travel_mode, ICON_CAR),
|
||||
key=ATTR_DURATION_IN_TRAFFIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
suggested_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
translation_key="distance",
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/holiday",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["holidays==0.74", "babel==2.15.0"]
|
||||
"requirements": ["holidays==0.75", "babel==2.15.0"]
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
"requirements": ["aiohomeconnect==0.17.1"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiohomeconnect==0.18.0"],
|
||||
"zeroconf": ["_homeconnect._tcp.local."]
|
||||
}
|
||||
|
||||
71
homeassistant/components/home_connect/quality_scale.yaml
Normal file
71
homeassistant/components/home_connect/quality_scale.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup: done
|
||||
appropriate-polling:
|
||||
status: done
|
||||
comment: |
|
||||
Full polling is performed at the configuration entry setup and
|
||||
device polling is performed when a CONNECTED or a PAIRED event is received.
|
||||
If many CONNECTED or PAIRED events are received for a device within a short time span,
|
||||
the integration will stop polling for that device and will create a repair issue.
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: done
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: done
|
||||
discovery-update-info: done
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default:
|
||||
status: done
|
||||
comment: |
|
||||
Event entities are disabled by default to prevent user confusion regarding
|
||||
which events are supported by its appliance.
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow:
|
||||
status: exempt
|
||||
comment: |
|
||||
This integration doesn't have settings in its configuration flow.
|
||||
repair-issues: done
|
||||
stale-devices: done
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: done
|
||||
@@ -309,8 +309,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Set up the options flow."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
@@ -451,16 +450,11 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Configure the Silicon Labs Multiprotocol add-on."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.silabs_multiprotocol import (
|
||||
from homeassistant.components.zha.silabs_multiprotocol import ( # noqa: PLC0415
|
||||
async_get_channel as async_get_zha_channel,
|
||||
)
|
||||
|
||||
@@ -747,11 +741,8 @@ class OptionsFlowHandler(OptionsFlow, ABC):
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform initial backup and reconfigure ZHA."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.zha.radio_manager import (
|
||||
from homeassistant.components.zha import DOMAIN as ZHA_DOMAIN # noqa: PLC0415
|
||||
from homeassistant.components.zha.radio_manager import ( # noqa: PLC0415
|
||||
ZhaMultiPANMigrationHelper,
|
||||
)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeeConfigEntry) -> boo
|
||||
entry.runtime_data = homee
|
||||
entry.async_on_unload(homee.disconnect)
|
||||
|
||||
def _connection_update_callback(connected: bool) -> None:
|
||||
async def _connection_update_callback(connected: bool) -> None:
|
||||
"""Call when the device is notified of changes."""
|
||||
if connected:
|
||||
_LOGGER.warning("Reconnected to Homee at %s", entry.data[CONF_HOST])
|
||||
|
||||
@@ -28,6 +28,7 @@ class HomeeEntity(Entity):
|
||||
self._entry = entry
|
||||
node = entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||
# Homee hub itself has node-id -1
|
||||
assert node is not None
|
||||
if node.id == -1:
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, entry.runtime_data.settings.uid)},
|
||||
@@ -79,7 +80,7 @@ class HomeeEntity(Entity):
|
||||
def _on_node_updated(self, attribute: HomeeAttribute) -> None:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_connection_changed(self, connected: bool) -> None:
|
||||
async def _on_connection_changed(self, connected: bool) -> None:
|
||||
self._host_connected = connected
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -166,6 +167,6 @@ class HomeeNodeEntity(Entity):
|
||||
def _on_node_updated(self, node: HomeeNode) -> None:
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
def _on_connection_changed(self, connected: bool) -> None:
|
||||
async def _on_connection_changed(self, connected: bool) -> None:
|
||||
self._host_connected = connected
|
||||
self.schedule_update_ha_state()
|
||||
|
||||
@@ -58,9 +58,13 @@ class HomeeLock(HomeeEntity, LockEntity):
|
||||
AttributeChangedBy, self._attribute.changed_by
|
||||
)
|
||||
if self._attribute.changed_by == AttributeChangedBy.USER:
|
||||
changed_id = self._entry.runtime_data.get_user_by_id(
|
||||
user = self._entry.runtime_data.get_user_by_id(
|
||||
self._attribute.changed_by_id
|
||||
).username
|
||||
)
|
||||
if user is not None:
|
||||
changed_id = user.username
|
||||
else:
|
||||
changed_id = "Unknown"
|
||||
|
||||
return f"{changed_by_name}-{changed_id}"
|
||||
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["homee"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pyHomee==1.2.8"]
|
||||
"requirements": ["pyHomee==1.2.9"]
|
||||
}
|
||||
|
||||
@@ -177,9 +177,9 @@
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"upper": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
|
||||
"lower": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
|
||||
"released": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
|
||||
"upper": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::upper%]",
|
||||
"lower": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::lower%]",
|
||||
"released": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,7 @@
|
||||
"state_attributes": {
|
||||
"event_type": {
|
||||
"state": {
|
||||
"release": "[%key;component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
|
||||
"release": "[%key:component::homee::entity::event::button_state::state_attributes::event_type::state::released%]",
|
||||
"up": "Up",
|
||||
"down": "Down",
|
||||
"stop": "Stop",
|
||||
|
||||
@@ -28,6 +28,7 @@ def get_device_class(
|
||||
) -> SwitchDeviceClass:
|
||||
"""Check device class of Switch according to node profile."""
|
||||
node = config_entry.runtime_data.get_node_by_id(attribute.node_id)
|
||||
assert node is not None
|
||||
if node.profile in [
|
||||
NodeProfile.ON_OFF_PLUG,
|
||||
NodeProfile.METERING_PLUG,
|
||||
|
||||
@@ -355,11 +355,10 @@ class HomekitControllerFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_abort(reason="ignored_model")
|
||||
|
||||
# Late imports in case BLE is not available
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from aiohomekit.controller.ble.discovery import BleDiscovery
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from aiohomekit.controller.ble.manufacturer_data import HomeKitAdvertisement
|
||||
from aiohomekit.controller.ble.discovery import BleDiscovery # noqa: PLC0415
|
||||
from aiohomekit.controller.ble.manufacturer_data import ( # noqa: PLC0415
|
||||
HomeKitAdvertisement,
|
||||
)
|
||||
|
||||
mfr_data = discovery_info.manufacturer_data
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["homematicip"],
|
||||
"requirements": ["homematicip==2.0.5"]
|
||||
"requirements": ["homematicip==2.0.6"]
|
||||
}
|
||||
|
||||
@@ -23,9 +23,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
|
||||
|
||||
api: HomeWizardEnergy
|
||||
|
||||
is_battery = entry.unique_id.startswith("HWE-BAT") if entry.unique_id else False
|
||||
|
||||
if (token := entry.data.get(CONF_TOKEN)) and is_battery:
|
||||
if token := entry.data.get(CONF_TOKEN):
|
||||
api = HomeWizardEnergyV2(
|
||||
entry.data[CONF_IP_ADDRESS],
|
||||
token=token,
|
||||
@@ -37,8 +35,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: HomeWizardConfigEntry) -
|
||||
clientsession=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
if is_battery:
|
||||
await async_check_v2_support_and_create_issue(hass, entry)
|
||||
await async_check_v2_support_and_create_issue(hass, entry)
|
||||
|
||||
coordinator = HWEnergyDeviceUpdateCoordinator(hass, entry, api)
|
||||
try:
|
||||
|
||||
@@ -278,8 +278,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
ssl_certificate is not None
|
||||
and (hass.config.external_url or hass.config.internal_url) is None
|
||||
):
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.cloud import (
|
||||
from homeassistant.components.cloud import ( # noqa: PLC0415
|
||||
CloudNotAvailable,
|
||||
async_remote_ui_url,
|
||||
)
|
||||
@@ -511,12 +510,14 @@ class HomeAssistantHTTP:
|
||||
) -> None:
|
||||
"""Register a folder or file to serve as a static path."""
|
||||
frame.report_usage(
|
||||
"calls hass.http.register_static_path which is deprecated because "
|
||||
"it does blocking I/O in the event loop, instead "
|
||||
"calls hass.http.register_static_path which "
|
||||
"does blocking I/O in the event loop, instead "
|
||||
"call `await hass.http.async_register_static_paths("
|
||||
f'[StaticPathConfig("{url_path}", "{path}", {cache_headers})])`',
|
||||
exclude_integrations={"http"},
|
||||
core_behavior=frame.ReportBehavior.LOG,
|
||||
core_behavior=frame.ReportBehavior.ERROR,
|
||||
core_integration_behavior=frame.ReportBehavior.ERROR,
|
||||
custom_integration_behavior=frame.ReportBehavior.ERROR,
|
||||
breaks_in_ha_version="2025.7",
|
||||
)
|
||||
configs = [StaticPathConfig(url_path, path, cache_headers)]
|
||||
|
||||
@@ -136,8 +136,7 @@ async def process_wrong_login(request: Request) -> None:
|
||||
_LOGGER.warning(log_msg)
|
||||
|
||||
# Circular import with websocket_api
|
||||
# pylint: disable=import-outside-toplevel
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.components import persistent_notification # noqa: PLC0415
|
||||
|
||||
persistent_notification.async_create(
|
||||
hass, notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==2025.5.1"]
|
||||
"requirements": ["aioautomower==2025.6.0"]
|
||||
}
|
||||
|
||||
@@ -444,8 +444,9 @@ class TimerManager:
|
||||
timer.finish()
|
||||
|
||||
if timer.conversation_command:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.conversation import async_converse
|
||||
from homeassistant.components.conversation import ( # noqa: PLC0415
|
||||
async_converse,
|
||||
)
|
||||
|
||||
self.hass.async_create_background_task(
|
||||
async_converse(
|
||||
|
||||
@@ -2,15 +2,14 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
|
||||
from .const import DOMAIN, PLATFORMS
|
||||
from .coordinator import JustNimbusCoordinator
|
||||
from .const import PLATFORMS
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Set up JustNimbus from a config entry."""
|
||||
if "zip_code" in entry.data:
|
||||
coordinator = JustNimbusCoordinator(hass, entry)
|
||||
@@ -18,13 +17,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryAuthFailed
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
entry.runtime_data = coordinator
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: JustNimbusConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -16,13 +16,17 @@ from .const import CONF_ZIP_CODE, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type JustNimbusConfigEntry = ConfigEntry[JustNimbusCoordinator]
|
||||
|
||||
|
||||
class JustNimbusCoordinator(DataUpdateCoordinator[justnimbus.JustNimbusModel]):
|
||||
"""Data update coordinator."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: JustNimbusConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, config_entry: JustNimbusConfigEntry
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_CLIENT_ID,
|
||||
EntityCategory,
|
||||
@@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import JustNimbusCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import JustNimbusConfigEntry, JustNimbusCoordinator
|
||||
from .entity import JustNimbusEntity
|
||||
|
||||
|
||||
@@ -102,16 +100,15 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: JustNimbusConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the JustNimbus sensor."""
|
||||
coordinator: JustNimbusCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
async_add_entities(
|
||||
JustNimbusSensor(
|
||||
device_id=entry.data[CONF_CLIENT_ID],
|
||||
description=description,
|
||||
coordinator=coordinator,
|
||||
coordinator=entry.runtime_data,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
)
|
||||
|
||||
@@ -37,5 +37,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pylamarzocco"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["pylamarzocco==2.0.8"]
|
||||
"requirements": ["pylamarzocco==2.0.9"]
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ ENTITIES: tuple[LaMarzoccoNumberEntityDescription, ...] = (
|
||||
CoffeeBoiler, machine.dashboard.config[WidgetType.CM_COFFEE_BOILER]
|
||||
).target_temperature
|
||||
),
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoNumberEntityDescription(
|
||||
key="smart_standby_time",
|
||||
@@ -221,7 +225,7 @@ class LaMarzoccoNumberEntity(LaMarzoccoEntity, NumberEntity):
|
||||
entity_description: LaMarzoccoNumberEntityDescription
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
def native_value(self) -> float | int:
|
||||
"""Return the current value."""
|
||||
return self.entity_description.native_value_fn(self.coordinator.device)
|
||||
|
||||
|
||||
@@ -57,6 +57,10 @@ ENTITIES: tuple[LaMarzoccoSensorEntityDescription, ...] = (
|
||||
).ready_start_time
|
||||
),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
available_fn=(
|
||||
lambda coordinator: WidgetType.CM_COFFEE_BOILER
|
||||
in coordinator.device.dashboard.config
|
||||
),
|
||||
),
|
||||
LaMarzoccoSensorEntityDescription(
|
||||
key="steam_boiler_ready_time",
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from thinqconnect import DeviceType
|
||||
from thinqconnect.integration import ExtendedProperty
|
||||
@@ -154,7 +155,7 @@ class ThinQStateVacuumEntity(ThinQEntity, StateVacuumEntity):
|
||||
)
|
||||
)
|
||||
|
||||
async def async_return_to_base(self, **kwargs) -> None:
|
||||
async def async_return_to_base(self, **kwargs: Any) -> None:
|
||||
"""Return device to dock."""
|
||||
_LOGGER.debug(
|
||||
"[%s:%s] async_return_to_base",
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"],
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_todo",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["ical==10.0.0"]
|
||||
"requirements": ["ical==10.0.4"]
|
||||
}
|
||||
|
||||
@@ -1,93 +1,28 @@
|
||||
"""The Meater Temperature Probe integration."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from meater import (
|
||||
AuthenticationError,
|
||||
MeaterApi,
|
||||
ServiceUnavailableError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
from meater.MeaterApi import MeaterProbe
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MeaterConfigEntry, MeaterCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: MeaterConfigEntry) -> bool:
|
||||
"""Set up Meater Temperature Probe from a config entry."""
|
||||
# Store an API object to access
|
||||
session = async_get_clientsession(hass)
|
||||
meater_api = MeaterApi(session)
|
||||
|
||||
# Add the credentials
|
||||
try:
|
||||
_LOGGER.debug("Authenticating with the Meater API")
|
||||
await meater_api.authenticate(
|
||||
entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD]
|
||||
)
|
||||
except (ServiceUnavailableError, TooManyRequestsError) as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Unable to authenticate with the Meater API: {err}"
|
||||
) from err
|
||||
|
||||
async def async_update_data() -> dict[str, MeaterProbe]:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
# Note: TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
async with asyncio.timeout(10):
|
||||
devices: list[MeaterProbe] = await meater_api.get_all_devices()
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err
|
||||
except TooManyRequestsError as err:
|
||||
raise UpdateFailed(
|
||||
"Too many requests have been made to the API, rate limiting is in place"
|
||||
) from err
|
||||
|
||||
return {device.id: device for device in devices}
|
||||
|
||||
coordinator = DataUpdateCoordinator(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
# Name of the data. For logging purposes.
|
||||
name="meater_api",
|
||||
update_method=async_update_data,
|
||||
# Polling interval. Will only be polled if there are subscribers.
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
coordinator = MeaterCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN].setdefault("known_probes", set())
|
||||
hass.data.setdefault(DOMAIN, {}).setdefault("known_probes", set())
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
"api": meater_api,
|
||||
"coordinator": coordinator,
|
||||
}
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: MeaterConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
77
homeassistant/components/meater/coordinator.py
Normal file
77
homeassistant/components/meater/coordinator.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""Meater Coordinator."""
|
||||
|
||||
import asyncio
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from meater.MeaterApi import (
|
||||
AuthenticationError,
|
||||
MeaterApi,
|
||||
MeaterProbe,
|
||||
ServiceUnavailableError,
|
||||
TooManyRequestsError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type MeaterConfigEntry = ConfigEntry[MeaterCoordinator]
|
||||
|
||||
|
||||
class MeaterCoordinator(DataUpdateCoordinator[dict[str, MeaterProbe]]):
|
||||
"""Meater Coordinator."""
|
||||
|
||||
config_entry: MeaterConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
entry: MeaterConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the Meater Coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
name=f"Meater {entry.title}",
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
session = async_get_clientsession(hass)
|
||||
self.client = MeaterApi(session)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the Meater Coordinator."""
|
||||
try:
|
||||
_LOGGER.debug("Authenticating with the Meater API")
|
||||
await self.client.authenticate(
|
||||
self.config_entry.data[CONF_USERNAME],
|
||||
self.config_entry.data[CONF_PASSWORD],
|
||||
)
|
||||
except (ServiceUnavailableError, TooManyRequestsError) as err:
|
||||
raise UpdateFailed from err
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Unable to authenticate with the Meater API: {err}"
|
||||
) from err
|
||||
|
||||
async def _async_update_data(self) -> dict[str, MeaterProbe]:
|
||||
"""Fetch data from API endpoint."""
|
||||
try:
|
||||
# Note: TimeoutError and aiohttp.ClientError are already
|
||||
# handled by the data update coordinator.
|
||||
async with asyncio.timeout(10):
|
||||
devices: list[MeaterProbe] = await self.client.get_all_devices()
|
||||
except AuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err
|
||||
except TooManyRequestsError as err:
|
||||
raise UpdateFailed(
|
||||
"Too many requests have been made to the API, rate limiting is in place"
|
||||
) from err
|
||||
|
||||
return {device.id: device for device in devices}
|
||||
55
homeassistant/components/meater/diagnostics.py
Normal file
55
homeassistant/components/meater/diagnostics.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Diagnostics support for the Meater integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import MeaterConfigEntry
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: MeaterConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
identifier: {
|
||||
"id": probe.id,
|
||||
"internal_temperature": probe.internal_temperature,
|
||||
"ambient_temperature": probe.ambient_temperature,
|
||||
"time_updated": probe.time_updated.isoformat(),
|
||||
"cook": (
|
||||
{
|
||||
"id": probe.cook.id,
|
||||
"name": probe.cook.name,
|
||||
"state": probe.cook.state,
|
||||
"target_temperature": (
|
||||
probe.cook.target_temperature
|
||||
if hasattr(probe.cook, "target_temperature")
|
||||
else None
|
||||
),
|
||||
"peak_temperature": (
|
||||
probe.cook.peak_temperature
|
||||
if hasattr(probe.cook, "peak_temperature")
|
||||
else None
|
||||
),
|
||||
"time_remaining": (
|
||||
probe.cook.time_remaining
|
||||
if hasattr(probe.cook, "time_remaining")
|
||||
else None
|
||||
),
|
||||
"time_elapsed": (
|
||||
probe.cook.time_elapsed
|
||||
if hasattr(probe.cook, "time_elapsed")
|
||||
else None
|
||||
),
|
||||
}
|
||||
if probe.cook
|
||||
else None
|
||||
),
|
||||
}
|
||||
for identifier, probe in coordinator.data.items()
|
||||
}
|
||||
@@ -14,18 +14,28 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import MeaterCoordinator
|
||||
from .const import DOMAIN
|
||||
from .coordinator import MeaterConfigEntry
|
||||
|
||||
COOK_STATES = {
|
||||
"Not Started": "not_started",
|
||||
"Configured": "configured",
|
||||
"Started": "started",
|
||||
"Ready For Resting": "ready_for_resting",
|
||||
"Resting": "resting",
|
||||
"Slightly Underdone": "slightly_underdone",
|
||||
"Finished": "finished",
|
||||
"Slightly Overdone": "slightly_overdone",
|
||||
"OVERCOOK!": "overcooked",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -82,13 +92,13 @@ SENSOR_TYPES = (
|
||||
available=lambda probe: probe is not None and probe.cook is not None,
|
||||
value=lambda probe: probe.cook.name if probe.cook else None,
|
||||
),
|
||||
# One of Not Started, Configured, Started, Ready For Resting, Resting,
|
||||
# Slightly Underdone, Finished, Slightly Overdone, OVERCOOK!. Not translated.
|
||||
MeaterSensorEntityDescription(
|
||||
key="cook_state",
|
||||
translation_key="cook_state",
|
||||
available=lambda probe: probe is not None and probe.cook is not None,
|
||||
value=lambda probe: probe.cook.state if probe.cook else None,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=list(COOK_STATES.values()),
|
||||
value=lambda probe: COOK_STATES.get(probe.cook.state) if probe.cook else None,
|
||||
),
|
||||
# Target temperature
|
||||
MeaterSensorEntityDescription(
|
||||
@@ -137,13 +147,11 @@ SENSOR_TYPES = (
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: MeaterConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the entry."""
|
||||
coordinator: DataUpdateCoordinator[dict[str, MeaterProbe]] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]["coordinator"]
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@callback
|
||||
def async_update_data():
|
||||
@@ -174,11 +182,10 @@ async def async_setup_entry(
|
||||
|
||||
# Add a subscriber to the coordinator to discover new temperature probes
|
||||
coordinator.async_add_listener(async_update_data)
|
||||
async_update_data()
|
||||
|
||||
|
||||
class MeaterProbeTemperature(
|
||||
SensorEntity, CoordinatorEntity[DataUpdateCoordinator[dict[str, MeaterProbe]]]
|
||||
):
|
||||
class MeaterProbeTemperature(SensorEntity, CoordinatorEntity[MeaterCoordinator]):
|
||||
"""Meater Temperature Sensor Entity."""
|
||||
|
||||
entity_description: MeaterSensorEntityDescription
|
||||
|
||||
@@ -40,7 +40,18 @@
|
||||
"name": "Cooking"
|
||||
},
|
||||
"cook_state": {
|
||||
"name": "Cook state"
|
||||
"name": "Cook state",
|
||||
"state": {
|
||||
"not_started": "Not started",
|
||||
"configured": "Configured",
|
||||
"started": "Started",
|
||||
"ready_for_resting": "Ready for resting",
|
||||
"resting": "Resting",
|
||||
"slightly_underdone": "Slightly underdone",
|
||||
"finished": "Finished",
|
||||
"slightly_overdone": "Slightly overdone",
|
||||
"overcooked": "Overcooked"
|
||||
}
|
||||
},
|
||||
"cook_target_temp": {
|
||||
"name": "Target temperature"
|
||||
|
||||
@@ -814,19 +814,6 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Flag media player features that are supported."""
|
||||
return self._attr_supported_features
|
||||
|
||||
@property
|
||||
def supported_features_compat(self) -> MediaPlayerEntityFeature:
|
||||
"""Return the supported features as MediaPlayerEntityFeature.
|
||||
|
||||
Remove this compatibility shim in 2025.1 or later.
|
||||
"""
|
||||
features = self.supported_features
|
||||
if type(features) is int:
|
||||
new_features = MediaPlayerEntityFeature(features)
|
||||
self._report_deprecated_supported_features_values(new_features)
|
||||
return new_features
|
||||
return features
|
||||
|
||||
def turn_on(self) -> None:
|
||||
"""Turn the media player on."""
|
||||
raise NotImplementedError
|
||||
@@ -966,87 +953,85 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
@property
|
||||
def support_play(self) -> bool:
|
||||
"""Boolean if play is supported."""
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_pause(self) -> bool:
|
||||
"""Boolean if pause is supported."""
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PAUSE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_stop(self) -> bool:
|
||||
"""Boolean if stop is supported."""
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.STOP in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_seek(self) -> bool:
|
||||
"""Boolean if seek is supported."""
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SEEK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_set(self) -> bool:
|
||||
"""Boolean if setting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_volume_mute(self) -> bool:
|
||||
"""Boolean if muting volume is supported."""
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.VOLUME_MUTE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_previous_track(self) -> bool:
|
||||
"""Boolean if previous track command supported."""
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PREVIOUS_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_next_track(self) -> bool:
|
||||
"""Boolean if next track command supported."""
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.NEXT_TRACK in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_play_media(self) -> bool:
|
||||
"""Boolean if play media command supported."""
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.PLAY_MEDIA in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_source(self) -> bool:
|
||||
"""Boolean if select source command supported."""
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SELECT_SOURCE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_select_sound_mode(self) -> bool:
|
||||
"""Boolean if select sound mode command supported."""
|
||||
return (
|
||||
MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features_compat
|
||||
)
|
||||
return MediaPlayerEntityFeature.SELECT_SOUND_MODE in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_clear_playlist(self) -> bool:
|
||||
"""Boolean if clear playlist command supported."""
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.CLEAR_PLAYLIST in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_shuffle_set(self) -> bool:
|
||||
"""Boolean if shuffle is supported."""
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.SHUFFLE_SET in self.supported_features
|
||||
|
||||
@final
|
||||
@property
|
||||
def support_grouping(self) -> bool:
|
||||
"""Boolean if player grouping is supported."""
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features_compat
|
||||
return MediaPlayerEntityFeature.GROUPING in self.supported_features
|
||||
|
||||
async def async_toggle(self) -> None:
|
||||
"""Toggle the power on the media player."""
|
||||
@@ -1074,7 +1059,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level < 1
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
min(1, self.volume_level + self.volume_step)
|
||||
@@ -1092,7 +1077,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
if (
|
||||
self.volume_level is not None
|
||||
and self.volume_level > 0
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features_compat
|
||||
and MediaPlayerEntityFeature.VOLUME_SET in self.supported_features
|
||||
):
|
||||
await self.async_set_volume_level(
|
||||
max(0, self.volume_level - self.volume_step)
|
||||
@@ -1135,7 +1120,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
def capability_attributes(self) -> dict[str, Any]:
|
||||
"""Return capability attributes."""
|
||||
data: dict[str, Any] = {}
|
||||
supported_features = self.supported_features_compat
|
||||
supported_features = self.supported_features
|
||||
|
||||
if (
|
||||
source_list := self.source_list
|
||||
@@ -1364,7 +1349,7 @@ async def websocket_browse_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.BROWSE_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support browsing media"
|
||||
@@ -1447,7 +1432,7 @@ async def websocket_search_media(
|
||||
connection.send_error(msg["id"], "entity_not_found", "Entity not found")
|
||||
return
|
||||
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features_compat:
|
||||
if MediaPlayerEntityFeature.SEARCH_MEDIA not in player.supported_features:
|
||||
connection.send_message(
|
||||
websocket_api.error_message(
|
||||
msg["id"], ERR_NOT_SUPPORTED, "Player does not support searching media"
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import dns.asyncresolver
|
||||
import dns.rdata
|
||||
import dns.rdataclass
|
||||
import dns.rdatatype
|
||||
@@ -22,20 +23,23 @@ PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_dnspython_rdata_classes() -> None:
|
||||
"""Load dnspython rdata classes used by mcstatus."""
|
||||
def prevent_dnspython_blocking_operations() -> None:
|
||||
"""Prevent dnspython blocking operations by pre-loading required data."""
|
||||
|
||||
# Blocking import: https://github.com/rthalley/dnspython/issues/1083
|
||||
for rdtype in dns.rdatatype.RdataType:
|
||||
if not dns.rdatatype.is_metatype(rdtype) or rdtype == dns.rdatatype.OPT:
|
||||
dns.rdata.get_rdata_class(dns.rdataclass.IN, rdtype) # type: ignore[no-untyped-call]
|
||||
|
||||
# Blocking open: https://github.com/rthalley/dnspython/issues/1200
|
||||
dns.asyncresolver.get_default_resolver()
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: MinecraftServerConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Minecraft Server from a config entry."""
|
||||
|
||||
# Workaround to avoid blocking imports from dnspython (https://github.com/rthalley/dnspython/issues/1083)
|
||||
await hass.async_add_executor_job(load_dnspython_rdata_classes)
|
||||
await hass.async_add_executor_job(prevent_dnspython_blocking_operations)
|
||||
|
||||
# Create coordinator instance and store it.
|
||||
coordinator = MinecraftServerCoordinator(hass, entry)
|
||||
|
||||
@@ -354,8 +354,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
def write_dump() -> None:
|
||||
with open(hass.config.path("mqtt_dump.txt"), "w", encoding="utf8") as fp:
|
||||
for msg in messages:
|
||||
fp.write(",".join(msg) + "\n")
|
||||
fp.writelines([",".join(msg) + "\n" for msg in messages])
|
||||
|
||||
async def finish_dump(_: datetime) -> None:
|
||||
"""Write dump to file."""
|
||||
@@ -608,8 +607,7 @@ async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove MQTT config entry from a device."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation
|
||||
from . import device_automation # noqa: PLC0415
|
||||
|
||||
await device_automation.async_removed_from_device(hass, device_entry.id)
|
||||
return True
|
||||
|
||||
@@ -293,10 +293,9 @@ class MqttClientSetup:
|
||||
"""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
from paho.mqtt import client as mqtt # pylint: disable=import-outside-toplevel
|
||||
from paho.mqtt import client as mqtt # noqa: PLC0415
|
||||
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from .async_client import AsyncMQTTClient
|
||||
from .async_client import AsyncMQTTClient # noqa: PLC0415
|
||||
|
||||
config = self._config
|
||||
clean_session: bool | None = None
|
||||
@@ -524,8 +523,7 @@ class MQTT:
|
||||
"""Start the misc periodic."""
|
||||
assert self._misc_timer is None, "Misc periodic already started"
|
||||
_LOGGER.debug("%s: Starting client misc loop", self.config_entry.title)
|
||||
# pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
# Inner function to avoid having to check late import
|
||||
# each time the function is called.
|
||||
@@ -665,8 +663,7 @@ class MQTT:
|
||||
|
||||
async def async_connect(self, client_available: asyncio.Future[bool]) -> None:
|
||||
"""Connect to the host. Does not process messages yet."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
result: int | None = None
|
||||
self._available_future = client_available
|
||||
@@ -724,8 +721,7 @@ class MQTT:
|
||||
|
||||
async def _reconnect_loop(self) -> None:
|
||||
"""Reconnect to the MQTT server."""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
while True:
|
||||
if not self.connected:
|
||||
@@ -1228,7 +1224,7 @@ class MQTT:
|
||||
"""Handle a callback exception."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error returned from MQTT server: %s",
|
||||
@@ -1273,8 +1269,7 @@ class MQTT:
|
||||
) -> None:
|
||||
"""Wait for ACK from broker or raise on error."""
|
||||
if result_code != 0:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
@@ -1322,8 +1317,7 @@ class MQTT:
|
||||
|
||||
|
||||
def _matcher_for_topic(subscription: str) -> Callable[[str], bool]:
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from paho.mqtt.matcher import MQTTMatcher
|
||||
from paho.mqtt.matcher import MQTTMatcher # noqa: PLC0415
|
||||
|
||||
matcher = MQTTMatcher() # type: ignore[no-untyped-call]
|
||||
matcher[subscription] = True
|
||||
|
||||
@@ -3493,7 +3493,7 @@ def try_connection(
|
||||
"""Test if we can connect to an MQTT broker."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt # noqa: PLC0415
|
||||
|
||||
mqtt_client_setup = MqttClientSetup(user_input)
|
||||
mqtt_client_setup.setup()
|
||||
|
||||
@@ -640,8 +640,7 @@ async def cleanup_device_registry(
|
||||
entities, triggers or tags.
|
||||
"""
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_trigger, tag
|
||||
from . import device_trigger, tag # noqa: PLC0415
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
@@ -35,7 +35,6 @@ from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||
from homeassistant.helpers.service_info.mqtt import ReceivePayloadType
|
||||
from homeassistant.helpers.typing import ConfigType, VolSchemaType
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -48,7 +47,6 @@ from .const import (
|
||||
CONF_OPTIONS,
|
||||
CONF_STATE_TOPIC,
|
||||
CONF_SUGGESTED_DISPLAY_PRECISION,
|
||||
DOMAIN,
|
||||
PAYLOAD_NONE,
|
||||
)
|
||||
from .entity import MqttAvailabilityMixin, MqttEntity, async_setup_entity_entry_helper
|
||||
@@ -138,12 +136,9 @@ def validate_sensor_state_and_device_class_config(config: ConfigType) -> ConfigT
|
||||
device_class in DEVICE_CLASS_UNITS
|
||||
and unit_of_measurement not in DEVICE_CLASS_UNITS[device_class]
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"The unit of measurement `%s` is not valid "
|
||||
"together with device class `%s`. "
|
||||
"this will stop working in HA Core 2025.7.0",
|
||||
unit_of_measurement,
|
||||
device_class,
|
||||
raise vol.Invalid(
|
||||
f"The unit of measurement `{unit_of_measurement}` is not valid "
|
||||
f"together with device class `{device_class}`",
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -194,40 +189,8 @@ class MqttSensor(MqttEntity, RestoreSensor):
|
||||
None
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_check_uom(self) -> None:
|
||||
"""Check if the unit of measurement is valid with the device class."""
|
||||
if (
|
||||
self._discovery_data is not None
|
||||
or self.device_class is None
|
||||
or self.native_unit_of_measurement is None
|
||||
):
|
||||
return
|
||||
if (
|
||||
self.device_class in DEVICE_CLASS_UNITS
|
||||
and self.native_unit_of_measurement
|
||||
not in DEVICE_CLASS_UNITS[self.device_class]
|
||||
):
|
||||
async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
self.entity_id,
|
||||
issue_domain=sensor.DOMAIN,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
learn_more_url=URL_DOCS_SUPPORTED_SENSOR_UOM,
|
||||
translation_placeholders={
|
||||
"uom": self.native_unit_of_measurement,
|
||||
"device_class": self.device_class.value,
|
||||
"entity_id": self.entity_id,
|
||||
},
|
||||
translation_key="invalid_unit_of_measurement",
|
||||
breaks_in_ha_version="2025.7.0",
|
||||
)
|
||||
|
||||
async def mqtt_async_added_to_hass(self) -> None:
|
||||
"""Restore state for entities with expire_after set."""
|
||||
self.async_check_uom()
|
||||
last_state: State | None
|
||||
last_sensor_data: SensorExtraStoredData | None
|
||||
if (
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
"invalid_platform_config": {
|
||||
"title": "Invalid config found for MQTT {domain} item",
|
||||
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||
},
|
||||
"invalid_unit_of_measurement": {
|
||||
"title": "Sensor with invalid unit of measurement",
|
||||
"description": "Manual configured Sensor entity **{entity_id}** has a configured unit of measurement **{uom}** which is not valid with configured device class **{device_class}**. Make sure a valid unit of measurement is configured or remove the device class, and [reload](/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue."
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
@@ -163,16 +163,14 @@ async def async_forward_entry_setup_and_setup_discovery(
|
||||
tasks: list[asyncio.Task] = []
|
||||
if "device_automation" in new_platforms:
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import device_automation
|
||||
from . import device_automation # noqa: PLC0415
|
||||
|
||||
tasks.append(
|
||||
create_eager_task(device_automation.async_setup_entry(hass, config_entry))
|
||||
)
|
||||
if "tag" in new_platforms:
|
||||
# Local import to avoid circular dependencies
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from . import tag
|
||||
from . import tag # noqa: PLC0415
|
||||
|
||||
tasks.append(create_eager_task(tag.async_setup_entry(hass, config_entry)))
|
||||
if new_entity_platforms := (new_platforms - {"tag", "device_automation"}):
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mysensors",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["mysensors"],
|
||||
"requirements": ["pymysensors==0.24.0"]
|
||||
"requirements": ["pymysensors==0.25.0"]
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mystrom",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymystrom"],
|
||||
"requirements": ["python-mystrom==2.2.0"]
|
||||
"requirements": ["python-mystrom==2.4.0"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["nessclient"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["nessclient==1.1.2"]
|
||||
"requirements": ["nessclient==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -175,9 +175,7 @@ async def async_get_announce_addresses(hass: HomeAssistant) -> list[str]:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up network for Home Assistant."""
|
||||
# Avoid circular issue: http->network->websocket_api->http
|
||||
from .websocket import ( # pylint: disable=import-outside-toplevel
|
||||
async_register_websocket_commands,
|
||||
)
|
||||
from .websocket import async_register_websocket_commands # noqa: PLC0415
|
||||
|
||||
await async_get_network(hass)
|
||||
|
||||
|
||||
@@ -13,14 +13,13 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
@@ -61,30 +60,14 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsBinarySensor(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[ConnectionStatus]], BinarySensorEntity
|
||||
):
|
||||
class NextDnsBinarySensor(NextDnsEntity, BinarySensorEntity):
|
||||
"""Define an NextDNS binary sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[ConnectionStatus],
|
||||
description: NextDnsBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self._attr_is_on = description.state(coordinator.data, coordinator.profile_id)
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_is_on = self.entity_description.state(
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if the binary sensor is on."""
|
||||
return self.entity_description.state(
|
||||
self.coordinator.data, self.coordinator.profile_id
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -4,21 +4,21 @@ from __future__ import annotations
|
||||
|
||||
from aiohttp import ClientError
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import AnalyticsStatus, ApiError, InvalidApiKeyError
|
||||
from nextdns import ApiError, InvalidApiKeyError
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
CLEAR_LOGS_BUTTON = ButtonEntityDescription(
|
||||
key="clear_logs",
|
||||
translation_key="clear_logs",
|
||||
@@ -37,24 +37,9 @@ async def async_setup_entry(
|
||||
async_add_entities([NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)])
|
||||
|
||||
|
||||
class NextDnsButton(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[AnalyticsStatus]], ButtonEntity
|
||||
):
|
||||
class NextDnsButton(NextDnsEntity, ButtonEntity):
|
||||
"""Define an NextDNS button."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[AnalyticsStatus],
|
||||
description: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Trigger cleaning logs."""
|
||||
try:
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aiohttp.client_exceptions import ClientConnectorError
|
||||
from nextdns import (
|
||||
@@ -24,7 +24,6 @@ from tenacity import RetryError
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -34,10 +33,10 @@ from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CoordinatorDataT = TypeVar("CoordinatorDataT", bound=NextDnsData)
|
||||
|
||||
|
||||
class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
class NextDnsUpdateCoordinator[CoordinatorDataT: NextDnsData](
|
||||
DataUpdateCoordinator[CoordinatorDataT]
|
||||
):
|
||||
"""Class to manage fetching NextDNS data API."""
|
||||
|
||||
config_entry: NextDnsConfigEntry
|
||||
@@ -53,14 +52,6 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
||||
"""Initialize."""
|
||||
self.nextdns = nextdns
|
||||
self.profile_id = profile_id
|
||||
self.profile_name = nextdns.get_profile_name(profile_id)
|
||||
self.device_info = DeviceInfo(
|
||||
configuration_url=f"https://my.nextdns.io/{profile_id}/setup",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(profile_id))},
|
||||
manufacturer="NextDNS Inc.",
|
||||
name=self.profile_name,
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
|
||||
35
homeassistant/components/nextdns/entity.py
Normal file
35
homeassistant/components/nextdns/entity.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Define NextDNS entities."""
|
||||
|
||||
from nextdns.model import NextDnsData
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
|
||||
|
||||
class NextDnsEntity[CoordinatorDataT: NextDnsData](
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]]
|
||||
):
|
||||
"""Define NextDNS entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[CoordinatorDataT],
|
||||
description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=f"https://my.nextdns.io/{coordinator.profile_id}/setup",
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, str(coordinator.profile_id))},
|
||||
manufacturer="NextDNS Inc.",
|
||||
name=coordinator.nextdns.get_profile_name(coordinator.profile_id),
|
||||
)
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self.entity_description = description
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Generic
|
||||
|
||||
from nextdns import (
|
||||
AnalyticsDnssec,
|
||||
@@ -13,6 +12,7 @@ from nextdns import (
|
||||
AnalyticsProtocols,
|
||||
AnalyticsStatus,
|
||||
)
|
||||
from nextdns.model import NextDnsData
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
@@ -20,10 +20,9 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import (
|
||||
@@ -33,14 +32,14 @@ from .const import (
|
||||
ATTR_PROTOCOLS,
|
||||
ATTR_STATUS,
|
||||
)
|
||||
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class NextDnsSensorEntityDescription(
|
||||
SensorEntityDescription, Generic[CoordinatorDataT]
|
||||
class NextDnsSensorEntityDescription[CoordinatorDataT: NextDnsData](
|
||||
SensorEntityDescription
|
||||
):
|
||||
"""NextDNS sensor entity description."""
|
||||
|
||||
@@ -297,27 +296,14 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsSensor(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[CoordinatorDataT]], SensorEntity
|
||||
class NextDnsSensor[CoordinatorDataT: NextDnsData](
|
||||
NextDnsEntity[CoordinatorDataT], SensorEntity
|
||||
):
|
||||
"""Define an NextDNS sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsSensorEntityDescription[CoordinatorDataT]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: NextDnsUpdateCoordinator[CoordinatorDataT],
|
||||
description: NextDnsSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
self._attr_native_value = description.value(coordinator.data)
|
||||
self.entity_description: NextDnsSensorEntityDescription = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
self._attr_native_value = self.entity_description.value(self.coordinator.data)
|
||||
self.async_write_ha_state()
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value(self.coordinator.data)
|
||||
|
||||
@@ -4,16 +4,25 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "The API key for your NextDNS account"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"data": {
|
||||
"profile": "Profile"
|
||||
"profile_name": "Profile"
|
||||
},
|
||||
"data_description": {
|
||||
"profile_name": "The NextDNS configuration profile you want to integrate"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,11 +15,11 @@ from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import NextDnsConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NextDnsUpdateCoordinator
|
||||
from .entity import NextDnsEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@@ -536,12 +536,9 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class NextDnsSwitch(
|
||||
CoordinatorEntity[NextDnsUpdateCoordinator[Settings]], SwitchEntity
|
||||
):
|
||||
class NextDnsSwitch(NextDnsEntity, SwitchEntity):
|
||||
"""Define an NextDNS switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
entity_description: NextDnsSwitchEntityDescription
|
||||
|
||||
def __init__(
|
||||
@@ -550,11 +547,8 @@ class NextDnsSwitch(
|
||||
description: NextDnsSwitchEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_device_info = coordinator.device_info
|
||||
self._attr_unique_id = f"{coordinator.profile_id}_{description.key}"
|
||||
super().__init__(coordinator, description)
|
||||
self._attr_is_on = description.state(coordinator.data)
|
||||
self.entity_description = description
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
|
||||
@@ -282,8 +282,7 @@ class BaseNotificationService:
|
||||
|
||||
for name, target in self.targets.items():
|
||||
target_name = slugify(f"{self._target_service_name_prefix}_{name}")
|
||||
if target_name in stale_targets:
|
||||
stale_targets.remove(target_name)
|
||||
stale_targets.discard(target_name)
|
||||
if (
|
||||
target_name in self.registered_targets
|
||||
and target == self.registered_targets[target_name]
|
||||
|
||||
@@ -322,8 +322,9 @@ class OllamaConversationEntity(
|
||||
num_keep = 2 * max_messages + 1
|
||||
drop_index = len(message_history.messages) - num_keep
|
||||
message_history.messages = [
|
||||
message_history.messages[0]
|
||||
] + message_history.messages[drop_index:]
|
||||
message_history.messages[0],
|
||||
*message_history.messages[drop_index:],
|
||||
]
|
||||
|
||||
async def _async_entry_update_listener(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry
|
||||
|
||||
@@ -218,8 +218,7 @@ class UserOnboardingView(_BaseOnboardingStepView):
|
||||
|
||||
# Return authorization code for fetching tokens and connect
|
||||
# during onboarding.
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.auth import create_auth_code
|
||||
from homeassistant.components.auth import create_auth_code # noqa: PLC0415
|
||||
|
||||
auth_code = create_auth_code(hass, data["client_id"], credentials)
|
||||
return self.json({"auth_code": auth_code})
|
||||
@@ -309,8 +308,7 @@ class IntegrationOnboardingView(_BaseOnboardingStepView):
|
||||
)
|
||||
|
||||
# Return authorization code so we can redirect user and log them in
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
from homeassistant.components.auth import create_auth_code
|
||||
from homeassistant.components.auth import create_auth_code # noqa: PLC0415
|
||||
|
||||
auth_code = create_auth_code(
|
||||
hass, data["client_id"], refresh_token.credential
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user