Merge branch 'dev' into mill

This commit is contained in:
Daniel Hjelseth Høyer 2024-12-03 18:35:12 +01:00 committed by GitHub
commit b3580d5cc4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1703 changed files with 37870 additions and 12990 deletions

View File

@ -94,7 +94,7 @@ jobs:
- name: Download nightly wheels of frontend - name: Download nightly wheels of frontend
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v7
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/frontend repo: home-assistant/frontend
@ -105,7 +105,7 @@ jobs:
- name: Download nightly wheels of intents - name: Download nightly wheels of intents
if: needs.init.outputs.channel == 'dev' if: needs.init.outputs.channel == 'dev'
uses: dawidd6/action-download-artifact@v6 uses: dawidd6/action-download-artifact@v7
with: with:
github_token: ${{secrets.GITHUB_TOKEN}} github_token: ${{secrets.GITHUB_TOKEN}}
repo: home-assistant/intents-package repo: home-assistant/intents-package
@ -509,7 +509,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image - name: Build Docker image
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile
@ -522,7 +522,7 @@ jobs:
- name: Push Docker image - name: Push Docker image
if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true' if: needs.init.outputs.channel != 'dev' && needs.init.outputs.publish == 'true'
id: push id: push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with: with:
context: . # So action will not pull the repository again context: . # So action will not pull the repository again
file: ./script/hassfest/docker/Dockerfile file: ./script/hassfest/docker/Dockerfile

View File

@ -40,7 +40,7 @@ env:
CACHE_VERSION: 11 CACHE_VERSION: 11
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 9 MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2024.12" HA_SHORT_VERSION: "2025.1"
DEFAULT_PYTHON: "3.12" DEFAULT_PYTHON: "3.12"
ALL_PYTHON_VERSIONS: "['3.12', '3.13']" ALL_PYTHON_VERSIONS: "['3.12', '3.13']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
@ -485,7 +485,6 @@ jobs:
uses: actions/cache@v4.1.2 uses: actions/cache@v4.1.2
with: with:
path: venv path: venv
lookup-only: true
key: >- key: >-
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }} needs.info.outputs.python_cache_key }}
@ -531,6 +530,26 @@ jobs:
python -m script.gen_requirements_all ci python -m script.gen_requirements_all ci
uv pip install -r requirements_all_pytest.txt -r requirements_test.txt uv pip install -r requirements_all_pytest.txt -r requirements_test.txt
uv pip install -e . --config-settings editable_mode=compat uv pip install -e . --config-settings editable_mode=compat
- name: Dump pip freeze
run: |
python -m venv venv
. venv/bin/activate
python --version
uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact
uses: actions/upload-artifact@v4.4.3
with:
name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt
overwrite: true
- name: Remove pip_freeze
run: rm pip_freeze.txt
- name: Remove generated requirements_all
if: steps.cache-venv.outputs.cache-hit != 'true'
run: rm requirements_all_pytest.txt requirements_all_wheels_*.txt
- name: Check dirty
run: |
./script/check_dirty
hassfest: hassfest:
name: Check hassfest name: Check hassfest
@ -819,6 +838,12 @@ jobs:
needs: needs:
- info - info
- base - base
- gen-requirements-all
- hassfest
- lint-other
- lint-ruff
- lint-ruff-format
- mypy
name: Split tests for full run name: Split tests for full run
steps: steps:
- name: Install additional OS dependencies - name: Install additional OS dependencies
@ -1248,7 +1273,7 @@ jobs:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'true' if: needs.info.outputs.test_full_suite == 'true'
uses: codecov/codecov-action@v5.0.2 uses: codecov/codecov-action@v5.0.7
with: with:
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
@ -1386,7 +1411,7 @@ jobs:
pattern: coverage-* pattern: coverage-*
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
if: needs.info.outputs.test_full_suite == 'false' if: needs.info.outputs.test_full_suite == 'false'
uses: codecov/codecov-action@v5.0.2 uses: codecov/codecov-action@v5.0.7
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}

View File

@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4.2.2
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3.27.4 uses: github/codeql-action/init@v3.27.5
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.27.4 uses: github/codeql-action/analyze@v3.27.5
with: with:
category: "/language:python" category: "/language:python"

View File

@ -143,7 +143,7 @@ jobs:
wheels-key: ${{ secrets.WHEELS_KEY }} wheels-key: ${{ secrets.WHEELS_KEY }}
env-file: true env-file: true
apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev"
skip-binary: aiohttp;multidict;yarl skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy
constraints: "homeassistant/package_constraints.txt" constraints: "homeassistant/package_constraints.txt"
requirements-diff: "requirements_diff.txt" requirements-diff: "requirements_diff.txt"
requirements: "requirements.txt" requirements: "requirements.txt"

View File

@ -1,6 +1,6 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4 rev: v0.8.1
hooks: hooks:
- id: ruff - id: ruff
args: args:
@ -83,7 +83,7 @@ repos:
pass_filenames: false pass_filenames: false
language: script language: script
types: [text] types: [text]
files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$ files: ^(homeassistant/.+/(icons|manifest|strings)\.json|homeassistant/.+/(quality_scale)\.yaml|homeassistant/brands/.*\.json|homeassistant/.+/services\.yaml|script/hassfest/(?!metadata|mypy_config).+\.py|requirements.+\.txt)$
- id: hassfest-metadata - id: hassfest-metadata
name: hassfest-metadata name: hassfest-metadata
entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker entry: script/run-in-env.sh python3 -m script.hassfest -p metadata,docker

View File

@ -41,6 +41,7 @@ homeassistant.util.unit_system
# --- Add components below this line --- # --- Add components below this line ---
homeassistant.components homeassistant.components
homeassistant.components.abode.* homeassistant.components.abode.*
homeassistant.components.acaia.*
homeassistant.components.accuweather.* homeassistant.components.accuweather.*
homeassistant.components.acer_projector.* homeassistant.components.acer_projector.*
homeassistant.components.acmeda.* homeassistant.components.acmeda.*
@ -385,6 +386,7 @@ homeassistant.components.recollect_waste.*
homeassistant.components.recorder.* homeassistant.components.recorder.*
homeassistant.components.remote.* homeassistant.components.remote.*
homeassistant.components.renault.* homeassistant.components.renault.*
homeassistant.components.reolink.*
homeassistant.components.repairs.* homeassistant.components.repairs.*
homeassistant.components.rest.* homeassistant.components.rest.*
homeassistant.components.rest_command.* homeassistant.components.rest_command.*
@ -404,6 +406,7 @@ homeassistant.components.ruuvitag_ble.*
homeassistant.components.samsungtv.* homeassistant.components.samsungtv.*
homeassistant.components.scene.* homeassistant.components.scene.*
homeassistant.components.schedule.* homeassistant.components.schedule.*
homeassistant.components.schlage.*
homeassistant.components.scrape.* homeassistant.components.scrape.*
homeassistant.components.script.* homeassistant.components.script.*
homeassistant.components.search.* homeassistant.components.search.*
@ -437,6 +440,7 @@ homeassistant.components.starlink.*
homeassistant.components.statistics.* homeassistant.components.statistics.*
homeassistant.components.steamist.* homeassistant.components.steamist.*
homeassistant.components.stookalert.* homeassistant.components.stookalert.*
homeassistant.components.stookwijzer.*
homeassistant.components.stream.* homeassistant.components.stream.*
homeassistant.components.streamlabswater.* homeassistant.components.streamlabswater.*
homeassistant.components.stt.* homeassistant.components.stt.*

30
.vscode/tasks.json vendored
View File

@ -56,6 +56,20 @@
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{
"label": "Pre-commit",
"type": "shell",
"command": "pre-commit run --show-diff-on-failure",
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{ {
"label": "Pylint", "label": "Pylint",
"type": "shell", "type": "shell",
@ -87,6 +101,22 @@
}, },
"problemMatcher": [] "problemMatcher": []
}, },
{
"label": "Update syrupy snapshots",
"detail": "Update syrupy snapshots for a given integration.",
"type": "shell",
"command": "python3 -m pytest ./tests/components/${input:integrationName} --snapshot-update",
"dependsOn": ["Compile English translations"],
"group": {
"kind": "test",
"isDefault": true
},
"presentation": {
"reveal": "always",
"panel": "new"
},
"problemMatcher": []
},
{ {
"label": "Generate Requirements", "label": "Generate Requirements",
"type": "shell", "type": "shell",

View File

@ -588,8 +588,8 @@ build.json @home-assistant/supervisor
/tests/components/group/ @home-assistant/core /tests/components/group/ @home-assistant/core
/homeassistant/components/guardian/ @bachya /homeassistant/components/guardian/ @bachya
/tests/components/guardian/ @bachya /tests/components/guardian/ @bachya
/homeassistant/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r /homeassistant/components/habitica/ @tr4nt0r
/tests/components/habitica/ @ASMfreaK @leikoilja @tr4nt0r /tests/components/habitica/ @tr4nt0r
/homeassistant/components/hardkernel/ @home-assistant/core /homeassistant/components/hardkernel/ @home-assistant/core
/tests/components/hardkernel/ @home-assistant/core /tests/components/hardkernel/ @home-assistant/core
/homeassistant/components/hardware/ @home-assistant/core /homeassistant/components/hardware/ @home-assistant/core
@ -1004,6 +1004,8 @@ build.json @home-assistant/supervisor
/tests/components/nice_go/ @IceBotYT /tests/components/nice_go/ @IceBotYT
/homeassistant/components/nightscout/ @marciogranzotto /homeassistant/components/nightscout/ @marciogranzotto
/tests/components/nightscout/ @marciogranzotto /tests/components/nightscout/ @marciogranzotto
/homeassistant/components/niko_home_control/ @VandeurenGlenn
/tests/components/niko_home_control/ @VandeurenGlenn
/homeassistant/components/nilu/ @hfurubotten /homeassistant/components/nilu/ @hfurubotten
/homeassistant/components/nina/ @DeerMaximum /homeassistant/components/nina/ @DeerMaximum
/tests/components/nina/ @DeerMaximum /tests/components/nina/ @DeerMaximum
@ -1573,6 +1575,8 @@ build.json @home-assistant/supervisor
/tests/components/unifi/ @Kane610 /tests/components/unifi/ @Kane610
/homeassistant/components/unifi_direct/ @tofuSCHNITZEL /homeassistant/components/unifi_direct/ @tofuSCHNITZEL
/homeassistant/components/unifiled/ @florisvdk /homeassistant/components/unifiled/ @florisvdk
/homeassistant/components/unifiprotect/ @RaHehl
/tests/components/unifiprotect/ @RaHehl
/homeassistant/components/upb/ @gwww /homeassistant/components/upb/ @gwww
/tests/components/upb/ @gwww /tests/components/upb/ @gwww
/homeassistant/components/upc_connect/ @pvizeli @fabaff /homeassistant/components/upc_connect/ @pvizeli @fabaff

View File

@ -13,7 +13,7 @@ ENV \
ARG QEMU_CPU ARG QEMU_CPU
# Install uv # Install uv
RUN pip3 install uv==0.5.0 RUN pip3 install uv==0.5.4
WORKDIR /usr/src WORKDIR /usr/src

View File

@ -18,7 +18,7 @@ from homeassistant.util.json import json_loads
JWT_TOKEN_CACHE_SIZE = 16 JWT_TOKEN_CACHE_SIZE = 16
MAX_TOKEN_SIZE = 8192 MAX_TOKEN_SIZE = 8192
_VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss") _VERIFY_KEYS = ("signature", "exp", "nbf", "iat", "aud", "iss", "sub", "jti")
_VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | { _VERIFY_OPTIONS: dict[str, Any] = {f"verify_{key}": True for key in _VERIFY_KEYS} | {
"require": [] "require": []

View File

@ -112,9 +112,6 @@ class AbodeFlowHandler(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if user_input is None: if user_input is None:
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=vol.Schema(self.data_schema) step_id="user", data_schema=vol.Schema(self.data_schema)

View File

@ -9,5 +9,6 @@
}, },
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["jaraco.abode", "lomond"], "loggers": ["jaraco.abode", "lomond"],
"requirements": ["jaraco.abode==6.2.1"] "requirements": ["jaraco.abode==6.2.1"],
"single_config_entry": true
} }

View File

@ -28,7 +28,6 @@
"invalid_mfa_code": "Invalid MFA code" "invalid_mfa_code": "Invalid MFA code"
}, },
"abort": { "abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },

View File

@ -13,6 +13,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .coordinator import AcaiaConfigEntry from .coordinator import AcaiaConfigEntry
from .entity import AcaiaEntity from .entity import AcaiaEntity
PARALLEL_UPDATES = 0
@dataclass(kw_only=True, frozen=True) @dataclass(kw_only=True, frozen=True)
class AcaiaButtonEntityDescription(ButtonEntityDescription): class AcaiaButtonEntityDescription(ButtonEntityDescription):

View File

@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
mac = format_mac(user_input[CONF_ADDRESS]) mac = user_input[CONF_ADDRESS]
try: try:
is_new_style_scale = await is_new_scale(mac) is_new_style_scale = await is_new_scale(mac)
except AcaiaDeviceNotFound: except AcaiaDeviceNotFound:
@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
except AcaiaUnknownDevice: except AcaiaUnknownDevice:
return self.async_abort(reason="unsupported_device") return self.async_abort(reason="unsupported_device")
else: else:
await self.async_set_unique_id(mac) await self.async_set_unique_id(format_mac(mac))
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
if not errors: if not errors:
return self.async_create_entry( return self.async_create_entry(
title=self._discovered_devices[user_input[CONF_ADDRESS]], title=self._discovered_devices[mac],
data={ data={
CONF_ADDRESS: mac, CONF_ADDRESS: mac,
CONF_IS_NEW_STYLE_SCALE: is_new_style_scale, CONF_IS_NEW_STYLE_SCALE: is_new_style_scale,
@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle a discovered Bluetooth device.""" """Handle a discovered Bluetooth device."""
self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address) self._discovered[CONF_ADDRESS] = discovery_info.address
self._discovered[CONF_NAME] = discovery_info.name self._discovered[CONF_NAME] = discovery_info.name
await self.async_set_unique_id(mac) await self.async_set_unique_id(format_mac(discovery_info.address))
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
try: try:

View File

@ -0,0 +1,31 @@
"""Diagnostics support for Acaia."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.core import HomeAssistant
from . import AcaiaConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: AcaiaConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
scale = coordinator.scale
# collect all data sources
return {
"model": scale.model,
"device_state": (
asdict(scale.device_state) if scale.device_state is not None else ""
),
"mac": scale.mac,
"last_disconnect_time": scale.last_disconnect_time,
"timer": scale.timer,
"weight": scale.weight,
}

View File

@ -2,7 +2,11 @@
from dataclasses import dataclass from dataclasses import dataclass
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import (
CONNECTION_BLUETOOTH,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -25,13 +29,15 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]):
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = entity_description self.entity_description = entity_description
self._scale = coordinator.scale self._scale = coordinator.scale
self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}" formatted_mac = format_mac(self._scale.mac)
self._attr_unique_id = f"{formatted_mac}_{entity_description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._scale.mac)}, identifiers={(DOMAIN, formatted_mac)},
manufacturer="Acaia", manufacturer="Acaia",
model=self._scale.model, model=self._scale.model,
suggested_area="Kitchen", suggested_area="Kitchen",
connections={(CONNECTION_BLUETOOTH, self._scale.mac)},
) )
@property @property

View File

@ -25,5 +25,5 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioacaia"], "loggers": ["aioacaia"],
"requirements": ["aioacaia==0.1.6"] "requirements": ["aioacaia==0.1.10"]
} }

View File

@ -0,0 +1,106 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
No custom actions are defined.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
No custom actions are defined.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: todo
entity-event-setup:
status: exempt
comment: |
No explicit event subscriptions.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup:
status: exempt
comment: |
Device is expected to be offline most of the time, but needs to connect quickly once available.
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
No custom actions are defined.
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable:
status: done
comment: |
Handled by coordinator.
parallel-updates: done
reauthentication-flow:
status: exempt
comment: |
No authentication required.
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
No IP discovery.
discovery:
status: done
comment: |
Bluetooth discovery.
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:
status: exempt
comment: |
Device type integration.
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
No noisy/non-essential entities.
entity-translations: done
exception-translations:
status: exempt
comment: |
No custom exceptions.
icon-translations: done
reconfiguration-flow:
status: exempt
comment: |
Only parameter that could be changed (MAC = unique_id) would force a new config entry.
repair-issues:
status: exempt
comment: |
No repairs/issues.
stale-devices:
status: exempt
comment: |
Device type integration.
# Platinum
async-dependency: done
inject-websession:
status: exempt
comment: |
Bluetooth connection.
strict-typing: done

View File

@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
SensorExtraStoredData, SensorExtraStoredData,
SensorStateClass, SensorStateClass,
) )
from homeassistant.const import PERCENTAGE, UnitOfMass from homeassistant.const import PERCENTAGE, UnitOfMass, UnitOfVolumeFlowRate
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -49,6 +49,14 @@ SENSORS: tuple[AcaiaSensorEntityDescription, ...] = (
), ),
value_fn=lambda scale: scale.weight, value_fn=lambda scale: scale.weight,
), ),
AcaiaDynamicUnitSensorEntityDescription(
key="flow_rate",
device_class=SensorDeviceClass.VOLUME_FLOW_RATE,
native_unit_of_measurement=UnitOfVolumeFlowRate.MILLILITERS_PER_SECOND,
suggested_display_precision=1,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda scale: scale.flow_rate,
),
) )
RESTORE_SENSORS: tuple[AcaiaSensorEntityDescription, ...] = ( RESTORE_SENSORS: tuple[AcaiaSensorEntityDescription, ...] = (
AcaiaSensorEntityDescription( AcaiaSensorEntityDescription(

View File

@ -18,6 +18,9 @@
"description": "[%key:component::bluetooth::config::step::user::description%]", "description": "[%key:component::bluetooth::config::step::user::description%]",
"data": { "data": {
"address": "[%key:common::config_flow::data::device%]" "address": "[%key:common::config_flow::data::device%]"
},
"data_description": {
"address": "Select Acaia scale you want to set up"
} }
} }
} }

View File

@ -7,7 +7,6 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["accuweather"], "loggers": ["accuweather"],
"quality_scale": "platinum",
"requirements": ["accuweather==4.0.0"], "requirements": ["accuweather==4.0.0"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -4,5 +4,6 @@
"codeowners": [], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/acer_projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["pyserial==3.5"] "requirements": ["pyserial==3.5"]
} }

View File

@ -3,5 +3,6 @@
"name": "Actiontec", "name": "Actiontec",
"codeowners": [], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/actiontec", "documentation": "https://www.home-assistant.io/integrations/actiontec",
"iot_class": "local_polling" "iot_class": "local_polling",
"quality_scale": "legacy"
} }

View File

@ -37,7 +37,7 @@ STATE_KEY_POSITION = "position"
PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend( PLATFORM_SCHEMA = COVER_PLATFORM_SCHEMA.extend(
{ {
vol.Optional(CONF_ADS_VAR): cv.string, vol.Required(CONF_ADS_VAR): cv.string,
vol.Optional(CONF_ADS_VAR_POSITION): cv.string, vol.Optional(CONF_ADS_VAR_POSITION): cv.string,
vol.Optional(CONF_ADS_VAR_SET_POS): cv.string, vol.Optional(CONF_ADS_VAR_SET_POS): cv.string,
vol.Optional(CONF_ADS_VAR_CLOSE): cv.string, vol.Optional(CONF_ADS_VAR_CLOSE): cv.string,

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/ads", "documentation": "https://www.home-assistant.io/integrations/ads",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyads"], "loggers": ["pyads"],
"quality_scale": "legacy",
"requirements": ["pyads==3.4.0"] "requirements": ["pyads==3.4.0"]
} }

View File

@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/advantage_air", "documentation": "https://www.home-assistant.io/integrations/advantage_air",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["advantage_air"], "loggers": ["advantage_air"],
"quality_scale": "platinum",
"requirements": ["advantage-air==0.4.4"] "requirements": ["advantage-air==0.4.4"]
} }

View File

@ -3,7 +3,7 @@
import logging import logging
from aemet_opendata.exceptions import AemetError, TownNotFound from aemet_opendata.exceptions import AemetError, TownNotFound
from aemet_opendata.interface import AEMET, ConnectionOptions from aemet_opendata.interface import AEMET, ConnectionOptions, UpdateFeature
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
@ -23,9 +23,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: AemetConfigEntry) -> boo
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
latitude = entry.data[CONF_LATITUDE] latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE] longitude = entry.data[CONF_LONGITUDE]
station_updates = entry.options.get(CONF_STATION_UPDATES, True) update_features: int = UpdateFeature.FORECAST
if entry.options.get(CONF_STATION_UPDATES, True):
update_features |= UpdateFeature.STATION
options = ConnectionOptions(api_key, station_updates) options = ConnectionOptions(api_key, update_features)
aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options) aemet = AEMET(aiohttp_client.async_get_clientsession(hass), options)
try: try:
await aemet.select_coordinates(latitude, longitude) await aemet.select_coordinates(latitude, longitude)

View File

@ -45,7 +45,7 @@ class AemetConfigFlow(ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(f"{latitude}-{longitude}") await self.async_set_unique_id(f"{latitude}-{longitude}")
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
options = ConnectionOptions(user_input[CONF_API_KEY], False) options = ConnectionOptions(user_input[CONF_API_KEY])
aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options) aemet = AEMET(aiohttp_client.async_get_clientsession(self.hass), options)
try: try:
await aemet.select_coordinates(latitude, longitude) await aemet.select_coordinates(latitude, longitude)

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aemet_opendata"], "loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.5.4"] "requirements": ["AEMET-OpenData==0.6.3"]
} }

View File

@ -0,0 +1,80 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
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: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: |
This integration does not require authentication.
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
This integration has a fixed single device.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: todo
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices:
status: exempt
comment: |
This integration has a fixed single device.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@ -7,6 +7,5 @@
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["airly"], "loggers": ["airly"],
"quality_scale": "platinum",
"requirements": ["airly==1.1.0"] "requirements": ["airly==1.1.0"]
} }

View File

@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairq"], "loggers": ["aioairq"],
"requirements": ["aioairq==0.3.2"] "requirements": ["aioairq==0.4.3"]
} }

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airtouch5", "documentation": "https://www.home-assistant.io/integrations/airtouch5",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["airtouch5py"], "loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.2.10"] "requirements": ["airtouch5py==0.2.11"]
} }

View File

@ -11,5 +11,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==0.9.6"] "requirements": ["aioairzone==0.9.7"]
} }

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta from datetime import timedelta
from functools import partial
import logging import logging
from typing import TYPE_CHECKING, Any, Final, final from typing import TYPE_CHECKING, Any, Final, final
@ -27,26 +26,14 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import ServiceValidationError
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.config_validation import make_entity_service_schema from homeassistant.helpers.config_validation import make_entity_service_schema
from homeassistant.helpers.deprecation import (
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from .const import ( # noqa: F401 from .const import (
_DEPRECATED_FORMAT_NUMBER,
_DEPRECATED_FORMAT_TEXT,
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY,
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
_DEPRECATED_SUPPORT_ALARM_ARM_HOME,
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT,
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION,
_DEPRECATED_SUPPORT_ALARM_TRIGGER,
ATTR_CHANGED_BY, ATTR_CHANGED_BY,
ATTR_CODE_ARM_REQUIRED, ATTR_CODE_ARM_REQUIRED,
DOMAIN, DOMAIN,
@ -163,7 +150,6 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
_alarm_control_panel_option_default_code: str | None = None _alarm_control_panel_option_default_code: str | None = None
__alarm_legacy_state: bool = False __alarm_legacy_state: bool = False
__alarm_legacy_state_reported: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None: def __init_subclass__(cls, **kwargs: Any) -> None:
"""Post initialisation processing.""" """Post initialisation processing."""
@ -173,17 +159,15 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
# setting the state directly. # setting the state directly.
cls.__alarm_legacy_state = True cls.__alarm_legacy_state = True
def __setattr__(self, __name: str, __value: Any) -> None: def __setattr__(self, name: str, value: Any, /) -> None:
"""Set attribute. """Set attribute.
Deprecation warning if setting '_attr_state' directly Deprecation warning if setting '_attr_state' directly
unless already reported. unless already reported.
""" """
if __name == "_attr_state": if name == "_attr_state":
if self.__alarm_legacy_state_reported is not True:
self._report_deprecated_alarm_state_handling() self._report_deprecated_alarm_state_handling()
self.__alarm_legacy_state_reported = True return super().__setattr__(name, value)
return super().__setattr__(__name, __value)
@callback @callback
def add_to_platform_start( def add_to_platform_start(
@ -194,7 +178,7 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
) -> None: ) -> None:
"""Start adding an entity to a platform.""" """Start adding an entity to a platform."""
super().add_to_platform_start(hass, platform, parallel_updates) super().add_to_platform_start(hass, platform, parallel_updates)
if self.__alarm_legacy_state and not self.__alarm_legacy_state_reported: if self.__alarm_legacy_state:
self._report_deprecated_alarm_state_handling() self._report_deprecated_alarm_state_handling()
@callback @callback
@ -203,18 +187,15 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
Integrations should implement alarm_state instead of using state directly. Integrations should implement alarm_state instead of using state directly.
""" """
self.__alarm_legacy_state_reported = True report_usage(
if "custom_components" in type(self).__module__: "is setting state directly."
# Do not report on core integrations as they have been fixed. f" Entity {self.entity_id} ({type(self)}) should implement the 'alarm_state'"
report_issue = "report it to the custom integration author." " property and return its state using the AlarmControlPanelState enum",
_LOGGER.warning( core_integration_behavior=ReportBehavior.ERROR,
"Entity %s (%s) is setting state directly" custom_integration_behavior=ReportBehavior.LOG,
" which will stop working in HA Core 2025.11." breaks_in_ha_version="2025.11",
" Entities should implement the 'alarm_state' property and" integration_domain=self.platform.platform_name if self.platform else None,
" return its state using the AlarmControlPanelState enum, please %s", exclude_integrations={DOMAIN},
self.entity_id,
type(self),
report_issue,
) )
@final @final
@ -275,7 +256,6 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
"""Check if arm code is required, raise if no code is given.""" """Check if arm code is required, raise if no code is given."""
if not (_code := self.code_or_default_code(code)) and self.code_arm_required: if not (_code := self.code_or_default_code(code)) and self.code_arm_required:
raise ServiceValidationError( raise ServiceValidationError(
f"Arming requires a code but none was given for {self.entity_id}",
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="code_arm_required", translation_key="code_arm_required",
translation_placeholders={ translation_placeholders={
@ -418,13 +398,3 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
self._alarm_control_panel_option_default_code = default_code self._alarm_control_panel_option_default_code = default_code
return return
self._alarm_control_panel_option_default_code = None self._alarm_control_panel_option_default_code = None
# As we import constants of the const module here, we need to add the following
# functions to check for deprecated constants again
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())

View File

@ -1,16 +1,8 @@
"""Provides the constants needed for component.""" """Provides the constants needed for component."""
from enum import IntFlag, StrEnum from enum import IntFlag, StrEnum
from functools import partial
from typing import Final from typing import Final
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
DOMAIN: Final = "alarm_control_panel" DOMAIN: Final = "alarm_control_panel"
ATTR_CHANGED_BY: Final = "changed_by" ATTR_CHANGED_BY: Final = "changed_by"
@ -39,12 +31,6 @@ class CodeFormat(StrEnum):
NUMBER = "number" NUMBER = "number"
# These constants are deprecated as of Home Assistant 2022.5, can be removed in 2025.1
# Please use the CodeFormat enum instead.
_DEPRECATED_FORMAT_TEXT: Final = DeprecatedConstantEnum(CodeFormat.TEXT, "2025.1")
_DEPRECATED_FORMAT_NUMBER: Final = DeprecatedConstantEnum(CodeFormat.NUMBER, "2025.1")
class AlarmControlPanelEntityFeature(IntFlag): class AlarmControlPanelEntityFeature(IntFlag):
"""Supported features of the alarm control panel entity.""" """Supported features of the alarm control panel entity."""
@ -56,27 +42,6 @@ class AlarmControlPanelEntityFeature(IntFlag):
ARM_VACATION = 32 ARM_VACATION = 32
# These constants are deprecated as of Home Assistant 2022.5
# Please use the AlarmControlPanelEntityFeature enum instead.
_DEPRECATED_SUPPORT_ALARM_ARM_HOME: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.ARM_HOME, "2025.1"
)
_DEPRECATED_SUPPORT_ALARM_ARM_AWAY: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.ARM_AWAY, "2025.1"
)
_DEPRECATED_SUPPORT_ALARM_ARM_NIGHT: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.ARM_NIGHT, "2025.1"
)
_DEPRECATED_SUPPORT_ALARM_TRIGGER: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.TRIGGER, "2025.1"
)
_DEPRECATED_SUPPORT_ALARM_ARM_CUSTOM_BYPASS: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS, "2025.1"
)
_DEPRECATED_SUPPORT_ALARM_ARM_VACATION: Final = DeprecatedConstantEnum(
AlarmControlPanelEntityFeature.ARM_VACATION, "2025.1"
)
CONDITION_TRIGGERED: Final = "is_triggered" CONDITION_TRIGGERED: Final = "is_triggered"
CONDITION_DISARMED: Final = "is_disarmed" CONDITION_DISARMED: Final = "is_disarmed"
CONDITION_ARMED_HOME: Final = "is_armed_home" CONDITION_ARMED_HOME: Final = "is_armed_home"
@ -84,10 +49,3 @@ CONDITION_ARMED_AWAY: Final = "is_armed_away"
CONDITION_ARMED_NIGHT: Final = "is_armed_night" CONDITION_ARMED_NIGHT: Final = "is_armed_night"
CONDITION_ARMED_VACATION: Final = "is_armed_vacation" CONDITION_ARMED_VACATION: Final = "is_armed_vacation"
CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass" CONDITION_ARMED_CUSTOM_BYPASS: Final = "is_armed_custom_bypass"
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())

View File

@ -130,7 +130,7 @@
}, },
"alarm_trigger": { "alarm_trigger": {
"name": "Trigger", "name": "Trigger",
"description": "Enables an external alarm trigger.", "description": "Trigger the alarm manually.",
"fields": { "fields": {
"code": { "code": {
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]", "name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
@ -138,5 +138,10 @@
} }
} }
} }
},
"exceptions": {
"code_arm_required": {
"message": "Arming requires a code but none was given for {entity_id}."
}
} }
} }

View File

@ -816,6 +816,12 @@ class AlexaPlaybackController(AlexaCapability):
""" """
supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_features = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
operations: dict[
cover.CoverEntityFeature | media_player.MediaPlayerEntityFeature, str
]
if self.entity.domain == cover.DOMAIN:
operations = {cover.CoverEntityFeature.STOP: "Stop"}
else:
operations = { operations = {
media_player.MediaPlayerEntityFeature.NEXT_TRACK: "Next", media_player.MediaPlayerEntityFeature.NEXT_TRACK: "Next",
media_player.MediaPlayerEntityFeature.PAUSE: "Pause", media_player.MediaPlayerEntityFeature.PAUSE: "Pause",

View File

@ -559,6 +559,10 @@ class CoverCapabilities(AlexaEntity):
) )
if supported & cover.CoverEntityFeature.SET_TILT_POSITION: if supported & cover.CoverEntityFeature.SET_TILT_POSITION:
yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt") yield AlexaRangeController(self.entity, instance=f"{cover.DOMAIN}.tilt")
if supported & (
cover.CoverEntityFeature.STOP | cover.CoverEntityFeature.STOP_TILT
):
yield AlexaPlaybackController(self.entity, instance=f"{cover.DOMAIN}.stop")
yield AlexaEndpointHealth(self.hass, self.entity) yield AlexaEndpointHealth(self.hass, self.entity)
yield Alexa(self.entity) yield Alexa(self.entity)

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import logging import logging
import math import math
@ -764,6 +765,22 @@ async def async_api_stop(
entity = directive.entity entity = directive.entity
data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id} data: dict[str, Any] = {ATTR_ENTITY_ID: entity.entity_id}
if entity.domain == cover.DOMAIN:
supported: int = entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
feature_services: dict[int, str] = {
cover.CoverEntityFeature.STOP.value: cover.SERVICE_STOP_COVER,
cover.CoverEntityFeature.STOP_TILT.value: cover.SERVICE_STOP_COVER_TILT,
}
await asyncio.gather(
*(
hass.services.async_call(
entity.domain, service, data, blocking=False, context=context
)
for feature, service in feature_services.items()
if feature & supported
)
)
else:
await hass.services.async_call( await hass.services.async_call(
entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context entity.domain, SERVICE_MEDIA_STOP, data, blocking=False, context=context
) )

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/alpha_vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["alpha_vantage"], "loggers": ["alpha_vantage"],
"quality_scale": "legacy",
"requirements": ["alpha-vantage==2.3.1"] "requirements": ["alpha-vantage==2.3.1"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/amazon_polly", "documentation": "https://www.home-assistant.io/integrations/amazon_polly",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["boto3", "botocore", "s3transfer"], "loggers": ["boto3", "botocore", "s3transfer"],
"quality_scale": "legacy",
"requirements": ["boto3==1.34.131"] "requirements": ["boto3==1.34.131"]
} }

View File

@ -1,7 +1,6 @@
"""Support for Amber Electric.""" """Support for Amber Electric."""
from amberelectric import Configuration import amberelectric
from amberelectric.api import amber_api
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_TOKEN from homeassistant.const import CONF_API_TOKEN
@ -15,8 +14,9 @@ type AmberConfigEntry = ConfigEntry[AmberUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AmberConfigEntry) -> bool:
"""Set up Amber Electric from a config entry.""" """Set up Amber Electric from a config entry."""
configuration = Configuration(access_token=entry.data[CONF_API_TOKEN]) configuration = amberelectric.Configuration(access_token=entry.data[CONF_API_TOKEN])
api_instance = amber_api.AmberApi.create(configuration) api_client = amberelectric.ApiClient(configuration)
api_instance = amberelectric.AmberApi(api_client)
site_id = entry.data[CONF_SITE_ID] site_id = entry.data[CONF_SITE_ID]
coordinator = AmberUpdateCoordinator(hass, api_instance, site_id) coordinator = AmberUpdateCoordinator(hass, api_instance, site_id)

View File

@ -3,8 +3,8 @@
from __future__ import annotations from __future__ import annotations
import amberelectric import amberelectric
from amberelectric.api import amber_api from amberelectric.models.site import Site
from amberelectric.model.site import Site, SiteStatus from amberelectric.models.site_status import SiteStatus
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@ -23,11 +23,15 @@ API_URL = "https://app.amber.com.au/developers"
def generate_site_selector_name(site: Site) -> str: def generate_site_selector_name(site: Site) -> str:
"""Generate the name to show in the site drop down in the configuration flow.""" """Generate the name to show in the site drop down in the configuration flow."""
# For some reason the generated API key returns this as any, not a string. Thanks pydantic
nmi = str(site.nmi)
if site.status == SiteStatus.CLOSED: if site.status == SiteStatus.CLOSED:
return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return] if site.closed_on is None:
return f"{nmi} (Closed)"
return f"{nmi} (Closed: {site.closed_on.isoformat()})"
if site.status == SiteStatus.PENDING: if site.status == SiteStatus.PENDING:
return site.nmi + " (Pending)" # type: ignore[no-any-return] return f"{nmi} (Pending)"
return site.nmi # type: ignore[no-any-return] return nmi
def filter_sites(sites: list[Site]) -> list[Site]: def filter_sites(sites: list[Site]) -> list[Site]:
@ -35,7 +39,7 @@ def filter_sites(sites: list[Site]) -> list[Site]:
filtered: list[Site] = [] filtered: list[Site] = []
filtered_nmi: set[str] = set() filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status.value): for site in sorted(sites, key=lambda site: site.status):
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi: if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site) filtered.append(site)
filtered_nmi.add(site.nmi) filtered_nmi.add(site.nmi)
@ -56,7 +60,8 @@ class AmberElectricConfigFlow(ConfigFlow, domain=DOMAIN):
def _fetch_sites(self, token: str) -> list[Site] | None: def _fetch_sites(self, token: str) -> list[Site] | None:
configuration = amberelectric.Configuration(access_token=token) configuration = amberelectric.Configuration(access_token=token)
api: amber_api.AmberApi = amber_api.AmberApi.create(configuration) api_client = amberelectric.ApiClient(configuration)
api = amberelectric.AmberApi(api_client)
try: try:
sites: list[Site] = filter_sites(api.get_sites()) sites: list[Site] = filter_sites(api.get_sites())

View File

@ -5,13 +5,13 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
from amberelectric import ApiException import amberelectric
from amberelectric.api import amber_api from amberelectric.models.actual_interval import ActualInterval
from amberelectric.model.actual_interval import ActualInterval from amberelectric.models.channel import ChannelType
from amberelectric.model.channel import ChannelType from amberelectric.models.current_interval import CurrentInterval
from amberelectric.model.current_interval import CurrentInterval from amberelectric.models.forecast_interval import ForecastInterval
from amberelectric.model.forecast_interval import ForecastInterval from amberelectric.models.price_descriptor import PriceDescriptor
from amberelectric.model.interval import Descriptor from amberelectric.rest import ApiException
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
@ -31,22 +31,22 @@ def is_forecast(interval: ActualInterval | CurrentInterval | ForecastInterval) -
def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool: def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is on the general channel.""" """Return true if the supplied interval is on the general channel."""
return interval.channel_type == ChannelType.GENERAL # type: ignore[no-any-return] return interval.channel_type == ChannelType.GENERAL
def is_controlled_load( def is_controlled_load(
interval: ActualInterval | CurrentInterval | ForecastInterval, interval: ActualInterval | CurrentInterval | ForecastInterval,
) -> bool: ) -> bool:
"""Return true if the supplied interval is on the controlled load channel.""" """Return true if the supplied interval is on the controlled load channel."""
return interval.channel_type == ChannelType.CONTROLLED_LOAD # type: ignore[no-any-return] return interval.channel_type == ChannelType.CONTROLLEDLOAD
def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool: def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
"""Return true if the supplied interval is on the feed in channel.""" """Return true if the supplied interval is on the feed in channel."""
return interval.channel_type == ChannelType.FEED_IN # type: ignore[no-any-return] return interval.channel_type == ChannelType.FEEDIN
def normalize_descriptor(descriptor: Descriptor) -> str | None: def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
"""Return the snake case versions of descriptor names. Returns None if the name is not recognized.""" """Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
if descriptor is None: if descriptor is None:
return None return None
@ -71,7 +71,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"""AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read.""" """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
def __init__( def __init__(
self, hass: HomeAssistant, api: amber_api.AmberApi, site_id: str self, hass: HomeAssistant, api: amberelectric.AmberApi, site_id: str
) -> None: ) -> None:
"""Initialise the data service.""" """Initialise the data service."""
super().__init__( super().__init__(
@ -93,12 +93,13 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
"grid": {}, "grid": {},
} }
try: try:
data = self._api.get_current_price(self.site_id, next=48) data = self._api.get_current_prices(self.site_id, next=48)
intervals = [interval.actual_instance for interval in data]
except ApiException as api_exception: except ApiException as api_exception:
raise UpdateFailed("Missing price data, skipping update") from api_exception raise UpdateFailed("Missing price data, skipping update") from api_exception
current = [interval for interval in data if is_current(interval)] current = [interval for interval in intervals if is_current(interval)]
forecasts = [interval for interval in data if is_forecast(interval)] forecasts = [interval for interval in intervals if is_forecast(interval)]
general = [interval for interval in current if is_general(interval)] general = [interval for interval in current if is_general(interval)]
if len(general) == 0: if len(general) == 0:
@ -137,7 +138,7 @@ class AmberUpdateCoordinator(DataUpdateCoordinator):
interval for interval in forecasts if is_feed_in(interval) interval for interval in forecasts if is_feed_in(interval)
] ]
LOGGER.debug("Fetched new Amber data: %s", data) LOGGER.debug("Fetched new Amber data: %s", intervals)
return result return result
async def _async_update_data(self) -> dict[str, Any]: async def _async_update_data(self) -> dict[str, Any]:

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric", "documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["amberelectric"], "loggers": ["amberelectric"],
"requirements": ["amberelectric==1.1.1"] "requirements": ["amberelectric==2.0.12"]
} }

View File

@ -8,9 +8,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from amberelectric.model.channel import ChannelType from amberelectric.models.channel import ChannelType
from amberelectric.model.current_interval import CurrentInterval from amberelectric.models.current_interval import CurrentInterval
from amberelectric.model.forecast_interval import ForecastInterval from amberelectric.models.forecast_interval import ForecastInterval
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
@ -52,7 +52,7 @@ class AmberSensor(CoordinatorEntity[AmberUpdateCoordinator], SensorEntity):
self, self,
coordinator: AmberUpdateCoordinator, coordinator: AmberUpdateCoordinator,
description: SensorEntityDescription, description: SensorEntityDescription,
channel_type: ChannelType, channel_type: str,
) -> None: ) -> None:
"""Initialize the Sensor.""" """Initialize the Sensor."""
super().__init__(coordinator) super().__init__(coordinator)
@ -73,7 +73,7 @@ class AmberPriceSensor(AmberSensor):
"""Return the current price in $/kWh.""" """Return the current price in $/kWh."""
interval = self.coordinator.data[self.entity_description.key][self.channel_type] interval = self.coordinator.data[self.entity_description.key][self.channel_type]
if interval.channel_type == ChannelType.FEED_IN: if interval.channel_type == ChannelType.FEEDIN:
return format_cents_to_dollars(interval.per_kwh) * -1 return format_cents_to_dollars(interval.per_kwh) * -1
return format_cents_to_dollars(interval.per_kwh) return format_cents_to_dollars(interval.per_kwh)
@ -87,9 +87,9 @@ class AmberPriceSensor(AmberSensor):
return data return data
data["duration"] = interval.duration data["duration"] = interval.duration
data["date"] = interval.date.isoformat() data["date"] = interval.var_date.isoformat()
data["per_kwh"] = format_cents_to_dollars(interval.per_kwh) data["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
if interval.channel_type == ChannelType.FEED_IN: if interval.channel_type == ChannelType.FEEDIN:
data["per_kwh"] = data["per_kwh"] * -1 data["per_kwh"] = data["per_kwh"] * -1
data["nem_date"] = interval.nem_time.isoformat() data["nem_date"] = interval.nem_time.isoformat()
data["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) data["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
@ -120,7 +120,7 @@ class AmberForecastSensor(AmberSensor):
return None return None
interval = intervals[0] interval = intervals[0]
if interval.channel_type == ChannelType.FEED_IN: if interval.channel_type == ChannelType.FEEDIN:
return format_cents_to_dollars(interval.per_kwh) * -1 return format_cents_to_dollars(interval.per_kwh) * -1
return format_cents_to_dollars(interval.per_kwh) return format_cents_to_dollars(interval.per_kwh)
@ -142,10 +142,10 @@ class AmberForecastSensor(AmberSensor):
for interval in intervals: for interval in intervals:
datum = {} datum = {}
datum["duration"] = interval.duration datum["duration"] = interval.duration
datum["date"] = interval.date.isoformat() datum["date"] = interval.var_date.isoformat()
datum["nem_date"] = interval.nem_time.isoformat() datum["nem_date"] = interval.nem_time.isoformat()
datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh) datum["per_kwh"] = format_cents_to_dollars(interval.per_kwh)
if interval.channel_type == ChannelType.FEED_IN: if interval.channel_type == ChannelType.FEEDIN:
datum["per_kwh"] = datum["per_kwh"] * -1 datum["per_kwh"] = datum["per_kwh"] * -1
datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh) datum["spot_per_kwh"] = format_cents_to_dollars(interval.spot_per_kwh)
datum["start_time"] = interval.start_time.isoformat() datum["start_time"] = interval.start_time.isoformat()

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["amcrest"], "loggers": ["amcrest"],
"quality_scale": "legacy",
"requirements": ["amcrest==1.9.8"] "requirements": ["amcrest==1.9.8"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/ampio", "documentation": "https://www.home-assistant.io/integrations/ampio",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["asmog"], "loggers": ["asmog"],
"quality_scale": "legacy",
"requirements": ["asmog==0.0.6"] "requirements": ["asmog==0.0.6"]
} }

View File

@ -9,7 +9,7 @@
"loggers": ["adb_shell", "androidtv", "pure_python_adb"], "loggers": ["adb_shell", "androidtv", "pure_python_adb"],
"requirements": [ "requirements": [
"adb-shell[async]==0.4.4", "adb-shell[async]==0.4.4",
"androidtv[async]==0.0.73", "androidtv[async]==0.0.75",
"pure-python-adb[async]==0.3.0.dev0" "pure-python-adb[async]==0.3.0.dev0"
] ]
} }

View File

@ -7,7 +7,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["androidtvremote2"], "loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.1.2"], "requirements": ["androidtvremote2==0.1.2"],
"zeroconf": ["_androidtvremote2._tcp.local."] "zeroconf": ["_androidtvremote2._tcp.local."]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl", "documentation": "https://www.home-assistant.io/integrations/anel_pwrctrl",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["anel_pwrctrl"], "loggers": ["anel_pwrctrl"],
"quality_scale": "legacy",
"requirements": ["anel-pwrctrl-homeassistant==0.0.1.dev2"] "requirements": ["anel-pwrctrl-homeassistant==0.0.1.dev2"]
} }

View File

@ -5,5 +5,5 @@
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith", "documentation": "https://www.home-assistant.io/integrations/aosmith",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.10"] "requirements": ["py-aosmith==1.0.11"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/apache_kafka", "documentation": "https://www.home-assistant.io/integrations/apache_kafka",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiokafka", "kafka_python"], "loggers": ["aiokafka", "kafka_python"],
"quality_scale": "legacy",
"requirements": ["aiokafka==0.10.0"] "requirements": ["aiokafka==0.10.0"]
} }

View File

@ -6,6 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["apcaccess"], "loggers": ["apcaccess"],
"quality_scale": "silver",
"requirements": ["aioapcaccess==0.4.2"] "requirements": ["aioapcaccess==0.4.2"]
} }

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/apple_tv", "documentation": "https://www.home-assistant.io/integrations/apple_tv",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyatv", "srptools"], "loggers": ["pyatv", "srptools"],
"requirements": ["pyatv==0.15.1"], "requirements": ["pyatv==0.16.0"],
"zeroconf": [ "zeroconf": [
"_mediaremotetv._tcp.local.", "_mediaremotetv._tcp.local.",
"_companion-link._tcp.local.", "_companion-link._tcp.local.",

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/apprise", "documentation": "https://www.home-assistant.io/integrations/apprise",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["apprise"], "loggers": ["apprise"],
"quality_scale": "legacy",
"requirements": ["apprise==1.9.0"] "requirements": ["apprise==1.9.0"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/aprs", "documentation": "https://www.home-assistant.io/integrations/aprs",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aprslib", "geographiclib", "geopy"], "loggers": ["aprslib", "geographiclib", "geopy"],
"quality_scale": "legacy",
"requirements": ["aprslib==0.7.2", "geopy==2.3.0"] "requirements": ["aprslib==0.7.2", "geopy==2.3.0"]
} }

View File

@ -5,12 +5,17 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData from APsystemsEZ1 import (
APsystemsEZ1M,
InverterReturnedError,
ReturnAlarmInfo,
ReturnOutputData,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import LOGGER from .const import DOMAIN, LOGGER
@dataclass @dataclass
@ -43,6 +48,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]):
self.api.min_power = device_info.minPower self.api.min_power = device_info.minPower
async def _async_update_data(self) -> ApSystemsSensorData: async def _async_update_data(self) -> ApSystemsSensorData:
try:
output_data = await self.api.get_output_data() output_data = await self.api.get_output_data()
alarm_info = await self.api.get_alarm_info() alarm_info = await self.api.get_alarm_info()
except InverterReturnedError:
raise UpdateFailed(
translation_domain=DOMAIN, translation_key="inverter_error"
) from None
return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info) return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info)

View File

@ -72,5 +72,10 @@
"name": "Inverter status" "name": "Inverter status"
} }
} }
},
"exceptions": {
"inverter_error": {
"message": "Inverter returned an error"
}
} }
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/aqualogic", "documentation": "https://www.home-assistant.io/integrations/aqualogic",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aqualogic"], "loggers": ["aqualogic"],
"quality_scale": "legacy",
"requirements": ["aqualogic==2.6"] "requirements": ["aqualogic==2.6"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/aquostv", "documentation": "https://www.home-assistant.io/integrations/aquostv",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["sharp_aquos_rc"], "loggers": ["sharp_aquos_rc"],
"quality_scale": "legacy",
"requirements": ["sharp_aquos_rc==0.3.2"] "requirements": ["sharp_aquos_rc==0.3.2"]
} }

View File

@ -3,5 +3,6 @@
"name": "aREST", "name": "aREST",
"codeowners": [], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/arest", "documentation": "https://www.home-assistant.io/integrations/arest",
"iot_class": "local_polling" "iot_class": "local_polling",
"quality_scale": "legacy"
} }

View File

@ -6,5 +6,6 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["arris_tg2492lg"], "loggers": ["arris_tg2492lg"],
"quality_scale": "legacy",
"requirements": ["arris-tg2492lg==2.2.0"] "requirements": ["arris-tg2492lg==2.2.0"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/aruba", "documentation": "https://www.home-assistant.io/integrations/aruba",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pexpect", "ptyprocess"], "loggers": ["pexpect", "ptyprocess"],
"quality_scale": "legacy",
"requirements": ["pexpect==4.6.0"] "requirements": ["pexpect==4.6.0"]
} }

View File

@ -4,5 +4,6 @@
"codeowners": [], "codeowners": [],
"dependencies": ["mqtt"], "dependencies": ["mqtt"],
"documentation": "https://www.home-assistant.io/integrations/arwn", "documentation": "https://www.home-assistant.io/integrations/arwn",
"iot_class": "local_polling" "iot_class": "local_polling",
"quality_scale": "legacy"
} }

View File

@ -1032,13 +1032,15 @@ class PipelineRun:
agent_id=self.intent_agent, agent_id=self.intent_agent,
) )
conversation_result: conversation.ConversationResult | None = None
if user_input.agent_id != conversation.HOME_ASSISTANT_AGENT:
# Sentence triggers override conversation agent # Sentence triggers override conversation agent
if ( if (
trigger_response_text trigger_response_text
:= await conversation.async_handle_sentence_triggers( := await conversation.async_handle_sentence_triggers(
self.hass, user_input self.hass, user_input
) )
): ) is not None:
# Sentence trigger matched # Sentence trigger matched
trigger_response = intent.IntentResponse( trigger_response = intent.IntentResponse(
self.pipeline.conversation_language self.pipeline.conversation_language
@ -1049,22 +1051,18 @@ class PipelineRun:
conversation_id=user_input.conversation_id, conversation_id=user_input.conversation_id,
) )
# Try local intents first, if preferred. # Try local intents first, if preferred.
# Skip this step if the default agent is already used. elif self.pipeline.prefer_local_intents and (
elif (
self.pipeline.prefer_local_intents
and (user_input.agent_id != conversation.HOME_ASSISTANT_AGENT)
and (
intent_response := await conversation.async_handle_intents( intent_response := await conversation.async_handle_intents(
self.hass, user_input self.hass, user_input
) )
)
): ):
# Local intent matched # Local intent matched
conversation_result = conversation.ConversationResult( conversation_result = conversation.ConversationResult(
response=intent_response, response=intent_response,
conversation_id=user_input.conversation_id, conversation_id=user_input.conversation_id,
) )
else:
if conversation_result is None:
# Fall back to pipeline conversation agent # Fall back to pipeline conversation agent
conversation_result = await conversation.async_converse( conversation_result = await conversation.async_converse(
hass=self.hass, hass=self.hass,

View File

@ -4,5 +4,6 @@
"codeowners": ["@mtdcr"], "codeowners": ["@mtdcr"],
"documentation": "https://www.home-assistant.io/integrations/aten_pe", "documentation": "https://www.home-assistant.io/integrations/aten_pe",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "legacy",
"requirements": ["atenpdu==0.3.2"] "requirements": ["atenpdu==0.3.2"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/atome", "documentation": "https://www.home-assistant.io/integrations/atome",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyatome"], "loggers": ["pyatome"],
"quality_scale": "legacy",
"requirements": ["pyAtome==0.1.1"] "requirements": ["pyAtome==0.1.1"]
} }

View File

@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.0"] "requirements": ["yalexs==8.10.0", "yalexs-ble==2.5.1"]
} }

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from typing import Any from typing import Any
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
@ -20,6 +21,12 @@ DATA_SCHEMA = vol.Schema(
} }
) )
STEP_REAUTH_SCHEMA = vol.Schema(
{
vol.Required(CONF_PASSWORD): str,
}
)
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN): class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Autarco.""" """Handle a config flow for Autarco."""
@ -55,3 +62,40 @@ class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors, errors=errors,
data_schema=DATA_SCHEMA, data_schema=DATA_SCHEMA,
) )
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication request from Autarco."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication confirmation."""
errors = {}
reauth_entry = self._get_reauth_entry()
if user_input is not None:
client = Autarco(
email=reauth_entry.data[CONF_EMAIL],
password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
try:
await client.get_account()
except AutarcoAuthenticationError:
errors["base"] = "invalid_auth"
except AutarcoConnectionError:
errors["base"] = "cannot_connect"
else:
return self.async_update_reload_and_abort(
reauth_entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
data_schema=STEP_REAUTH_SCHEMA,
errors=errors,
)

View File

@ -7,6 +7,7 @@ from typing import NamedTuple
from autarco import ( from autarco import (
AccountSite, AccountSite,
Autarco, Autarco,
AutarcoAuthenticationError,
AutarcoConnectionError, AutarcoConnectionError,
Battery, Battery,
Inverter, Inverter,
@ -16,6 +17,7 @@ from autarco import (
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL from .const import DOMAIN, LOGGER, SCAN_INTERVAL
@ -60,8 +62,10 @@ class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
inverters = await self.client.get_inverters(self.account_site.public_key) inverters = await self.client.get_inverters(self.account_site.public_key)
if site.has_battery: if site.has_battery:
battery = await self.client.get_battery(self.account_site.public_key) battery = await self.client.get_battery(self.account_site.public_key)
except AutarcoConnectionError as error: except AutarcoAuthenticationError as err:
raise UpdateFailed(error) from error raise ConfigEntryAuthFailed(err) from err
except AutarcoConnectionError as err:
raise UpdateFailed(err) from err
return AutarcoData( return AutarcoData(
solar=solar, solar=solar,
inverters=inverters, inverters=inverters,

View File

@ -0,0 +1,99 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules:
status: todo
comment: |
The entity.py file is not used in this integration.
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities of this integration does not explicitly subscribe to events.
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:
status: exempt
comment: |
This integration does not provide additional actions.
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: |
This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates:
status: exempt
comment: |
This integration only polls data using a coordinator.
Since the integration is read-only and poll-only (only provide sensor
data), there is no need to implement parallel updates.
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info:
status: exempt
comment: |
This integration cannot be discovered, it is a connecting to a service
provider, which uses the users home address to get the data.
discovery:
status: exempt
comment: |
This integration cannot be discovered, it is a connecting to a service
provider, which uses the users home address to get the data.
docs-data-update: done
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices:
status: exempt
comment: |
This is an service, which doesn't integrate with any devices.
docs-supported-functions: done
docs-troubleshooting: todo
docs-use-cases: done
dynamic-devices: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default:
status: exempt
comment: |
This integration does not have any entities that should disabled by default.
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration doesn't have any cases where raising an issue is needed.
stale-devices: todo
# Platinum
async-dependency: done
inject-websession: done
strict-typing: done

View File

@ -2,7 +2,7 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"description": "Connect to your Autarco account to get information about your solar panels.", "description": "Connect to your Autarco account, to get information about your sites.",
"data": { "data": {
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
@ -11,6 +11,16 @@
"email": "The email address of your Autarco account.", "email": "The email address of your Autarco account.",
"password": "The password of your Autarco account." "password": "The password of your Autarco account."
} }
},
"reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]",
"description": "The password for {email} is no longer valid.",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::autarco::config::step::user::data_description::password%]"
}
} }
}, },
"error": { "error": {
@ -18,7 +28,8 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]" "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
} }
}, },
"entity": { "entity": {

View File

@ -6,7 +6,6 @@ from abc import ABC, abstractmethod
import asyncio import asyncio
from collections.abc import Callable, Mapping from collections.abc import Callable, Mapping
from dataclasses import dataclass from dataclasses import dataclass
from functools import partial
import logging import logging
from typing import Any, Protocol, cast from typing import Any, Protocol, cast
@ -51,12 +50,6 @@ from homeassistant.core import (
from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError from homeassistant.exceptions import HomeAssistantError, ServiceNotFound, TemplateError
from homeassistant.helpers import condition from homeassistant.helpers import condition
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.deprecation import (
DeprecatedConstant,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity import ToggleEntity
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.issue_registry import ( from homeassistant.helpers.issue_registry import (
@ -86,12 +79,7 @@ from homeassistant.helpers.trace import (
trace_get, trace_get,
trace_path, trace_path,
) )
from homeassistant.helpers.trigger import ( from homeassistant.helpers.trigger import async_initialize_triggers
TriggerActionType,
TriggerData,
TriggerInfo,
async_initialize_triggers,
)
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from homeassistant.util.dt import parse_datetime from homeassistant.util.dt import parse_datetime
@ -137,20 +125,6 @@ class IfAction(Protocol):
"""AND all conditions.""" """AND all conditions."""
# AutomationActionType, AutomationTriggerData,
# and AutomationTriggerInfo are deprecated as of 2022.9.
# Can be removed in 2025.1
_DEPRECATED_AutomationActionType = DeprecatedConstant(
TriggerActionType, "TriggerActionType", "2025.1"
)
_DEPRECATED_AutomationTriggerData = DeprecatedConstant(
TriggerData, "TriggerData", "2025.1"
)
_DEPRECATED_AutomationTriggerInfo = DeprecatedConstant(
TriggerInfo, "TriggerInfo", "2025.1"
)
@bind_hass @bind_hass
def is_on(hass: HomeAssistant, entity_id: str) -> bool: def is_on(hass: HomeAssistant, entity_id: str) -> bool:
"""Return true if specified automation entity_id is on. """Return true if specified automation entity_id is on.
@ -477,6 +451,7 @@ class UnavailableAutomationEntity(BaseAutomationEntity):
) )
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
async_delete_issue( async_delete_issue(
self.hass, DOMAIN, f"{self.entity_id}_validation_{self._validation_status}" self.hass, DOMAIN, f"{self.entity_id}_validation_{self._validation_status}"
@ -1219,11 +1194,3 @@ def websocket_config(
"config": automation.raw_config, "config": automation.raw_config,
}, },
) )
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/avea", "documentation": "https://www.home-assistant.io/integrations/avea",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["avea"], "loggers": ["avea"],
"quality_scale": "legacy",
"requirements": ["avea==1.5.1"] "requirements": ["avea==1.5.1"]
} }

View File

@ -4,5 +4,6 @@
"codeowners": [], "codeowners": [],
"documentation": "https://www.home-assistant.io/integrations/avion", "documentation": "https://www.home-assistant.io/integrations/avion",
"iot_class": "assumed_state", "iot_class": "assumed_state",
"quality_scale": "legacy",
"requirements": ["avion==0.10"] "requirements": ["avion==0.10"]
} }

View File

@ -14,7 +14,4 @@ class AWSFlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import a config entry.""" """Import a config entry."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
return self.async_create_entry(title="configuration.yaml", data=import_data) return self.async_create_entry(title="configuration.yaml", data=import_data)

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/aws", "documentation": "https://www.home-assistant.io/integrations/aws",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aiobotocore", "botocore"], "loggers": ["aiobotocore", "botocore"],
"quality_scale": "legacy",
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"] "requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
} }

View File

@ -29,7 +29,6 @@
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["axis"], "loggers": ["axis"],
"quality_scale": "platinum",
"requirements": ["axis==63"], "requirements": ["axis==63"],
"ssdp": [ "ssdp": [
{ {

View File

@ -102,8 +102,6 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
"""Handle the initial user step.""" """Handle the initial user step."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if user_input is None: if user_input is None:
return self.async_show_form(step_id=STEP_USER, data_schema=BASE_SCHEMA) return self.async_show_form(step_id=STEP_USER, data_schema=BASE_SCHEMA)
@ -160,8 +158,6 @@ class AEHConfigFlow(ConfigFlow, domain=DOMAIN):
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult: async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
"""Import config from configuration.yaml.""" """Import config from configuration.yaml."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
if CONF_SEND_INTERVAL in import_data: if CONF_SEND_INTERVAL in import_data:
self._options[CONF_SEND_INTERVAL] = import_data.pop(CONF_SEND_INTERVAL) self._options[CONF_SEND_INTERVAL] = import_data.pop(CONF_SEND_INTERVAL)
if CONF_MAX_DELAY in import_data: if CONF_MAX_DELAY in import_data:

View File

@ -6,5 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["azure"], "loggers": ["azure"],
"requirements": ["azure-eventhub==5.11.1"] "requirements": ["azure-eventhub==5.11.1"],
"single_config_entry": true
} }

View File

@ -31,7 +31,6 @@
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"cannot_connect": "Connecting with the credentials from the configuration.yaml failed, please remove from yaml and use the config flow.", "cannot_connect": "Connecting with the credentials from the configuration.yaml failed, please remove from yaml and use the config flow.",
"unknown": "Connecting with the credentials from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow." "unknown": "Connecting with the credentials from the configuration.yaml failed with an unknown error, please remove from yaml and use the config flow."
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/azure_service_bus", "documentation": "https://www.home-assistant.io/integrations/azure_service_bus",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["azure"], "loggers": ["azure"],
"quality_scale": "legacy",
"requirements": ["azure-servicebus==7.10.0"] "requirements": ["azure-servicebus==7.10.0"]
} }

View File

@ -32,9 +32,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_handle_create_service(call: ServiceCall) -> None: async def async_handle_create_service(call: ServiceCall) -> None:
"""Service handler for creating backups.""" """Service handler for creating backups."""
await backup_manager.async_create_backup(on_progress=None) await backup_manager.async_create_backup()
if backup_task := backup_manager.backup_task:
await backup_task
hass.services.async_register(DOMAIN, "create", async_handle_create_service) hass.services.async_register(DOMAIN, "create", async_handle_create_service)

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import abc import abc
import asyncio import asyncio
from collections.abc import Callable
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
import hashlib import hashlib
import io import io
@ -35,13 +34,6 @@ from .const import DOMAIN, EXCLUDE_FROM_BACKUP, LOGGER
BUF_SIZE = 2**20 * 4 # 4MB BUF_SIZE = 2**20 * 4 # 4MB
@dataclass(slots=True)
class NewBackup:
"""New backup class."""
slug: str
@dataclass(slots=True) @dataclass(slots=True)
class Backup: class Backup:
"""Backup class.""" """Backup class."""
@ -57,15 +49,6 @@ class Backup:
return {**asdict(self), "path": self.path.as_posix()} return {**asdict(self), "path": self.path.as_posix()}
@dataclass(slots=True)
class BackupProgress:
"""Backup progress class."""
done: bool
stage: str | None
success: bool | None
class BackupPlatformProtocol(Protocol): class BackupPlatformProtocol(Protocol):
"""Define the format that backup platforms can have.""" """Define the format that backup platforms can have."""
@ -82,7 +65,7 @@ class BaseBackupManager(abc.ABC):
def __init__(self, hass: HomeAssistant) -> None: def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the backup manager.""" """Initialize the backup manager."""
self.hass = hass self.hass = hass
self.backup_task: asyncio.Task | None = None self.backing_up = False
self.backups: dict[str, Backup] = {} self.backups: dict[str, Backup] = {}
self.loaded_platforms = False self.loaded_platforms = False
self.platforms: dict[str, BackupPlatformProtocol] = {} self.platforms: dict[str, BackupPlatformProtocol] = {}
@ -150,12 +133,7 @@ class BaseBackupManager(abc.ABC):
"""Restore a backup.""" """Restore a backup."""
@abc.abstractmethod @abc.abstractmethod
async def async_create_backup( async def async_create_backup(self, **kwargs: Any) -> Backup:
self,
*,
on_progress: Callable[[BackupProgress], None] | None,
**kwargs: Any,
) -> NewBackup:
"""Generate a backup.""" """Generate a backup."""
@abc.abstractmethod @abc.abstractmethod
@ -314,36 +292,17 @@ class BackupManager(BaseBackupManager):
await self.hass.async_add_executor_job(_move_and_cleanup) await self.hass.async_add_executor_job(_move_and_cleanup)
await self.load_backups() await self.load_backups()
async def async_create_backup( async def async_create_backup(self, **kwargs: Any) -> Backup:
self,
*,
on_progress: Callable[[BackupProgress], None] | None,
**kwargs: Any,
) -> NewBackup:
"""Generate a backup.""" """Generate a backup."""
if self.backup_task: if self.backing_up:
raise HomeAssistantError("Backup already in progress") raise HomeAssistantError("Backup already in progress")
try:
self.backing_up = True
await self.async_pre_backup_actions()
backup_name = f"Core {HAVERSION}" backup_name = f"Core {HAVERSION}"
date_str = dt_util.now().isoformat() date_str = dt_util.now().isoformat()
slug = _generate_slug(date_str, backup_name) slug = _generate_slug(date_str, backup_name)
self.backup_task = self.hass.async_create_task(
self._async_create_backup(backup_name, date_str, slug, on_progress),
name="backup_manager_create_backup",
eager_start=False, # To ensure the task is not started before we return
)
return NewBackup(slug=slug)
async def _async_create_backup(
self,
backup_name: str,
date_str: str,
slug: str,
on_progress: Callable[[BackupProgress], None] | None,
) -> Backup:
"""Generate a backup."""
success = False
try:
await self.async_pre_backup_actions()
backup_data = { backup_data = {
"slug": slug, "slug": slug,
@ -370,12 +329,9 @@ class BackupManager(BaseBackupManager):
if self.loaded_backups: if self.loaded_backups:
self.backups[slug] = backup self.backups[slug] = backup
LOGGER.debug("Generated new backup with slug %s", slug) LOGGER.debug("Generated new backup with slug %s", slug)
success = True
return backup return backup
finally: finally:
if on_progress: self.backing_up = False
on_progress(BackupProgress(done=True, stage=None, success=success))
self.backup_task = None
await self.async_post_backup_actions() await self.async_post_backup_actions()
def _mkdir_and_generate_backup_contents( def _mkdir_and_generate_backup_contents(

View File

@ -7,5 +7,5 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "calculated", "iot_class": "calculated",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["securetar==2024.2.1"] "requirements": ["securetar==2024.11.0"]
} }

View File

@ -8,7 +8,6 @@ from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from .const import DATA_MANAGER, LOGGER from .const import DATA_MANAGER, LOGGER
from .manager import BackupProgress
@callback @callback
@ -41,7 +40,7 @@ async def handle_info(
msg["id"], msg["id"],
{ {
"backups": list(backups.values()), "backups": list(backups.values()),
"backing_up": manager.backup_task is not None, "backing_up": manager.backing_up,
}, },
) )
@ -114,11 +113,7 @@ async def handle_create(
msg: dict[str, Any], msg: dict[str, Any],
) -> None: ) -> None:
"""Generate a backup.""" """Generate a backup."""
backup = await hass.data[DATA_MANAGER].async_create_backup()
def on_progress(progress: BackupProgress) -> None:
connection.send_message(websocket_api.event_message(msg["id"], progress))
backup = await hass.data[DATA_MANAGER].async_create_backup(on_progress=on_progress)
connection.send_result(msg["id"], backup) connection.send_result(msg["id"], backup)
@ -132,6 +127,7 @@ async def handle_backup_start(
) -> None: ) -> None:
"""Backup start notification.""" """Backup start notification."""
manager = hass.data[DATA_MANAGER] manager = hass.data[DATA_MANAGER]
manager.backing_up = True
LOGGER.debug("Backup start notification") LOGGER.debug("Backup start notification")
try: try:
@ -153,6 +149,7 @@ async def handle_backup_end(
) -> None: ) -> None:
"""Backup end notification.""" """Backup end notification."""
manager = hass.data[DATA_MANAGER] manager = hass.data[DATA_MANAGER]
manager.backing_up = False
LOGGER.debug("Backup end notification") LOGGER.debug("Backup end notification")
try: try:

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/baidu", "documentation": "https://www.home-assistant.io/integrations/baidu",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aip"], "loggers": ["aip"],
"quality_scale": "legacy",
"requirements": ["baidu-aip==1.6.6"] "requirements": ["baidu-aip==1.6.6"]
} }

View File

@ -0,0 +1,40 @@
"""Support for Bang & Olufsen diagnostics."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
from homeassistant.core import HomeAssistant
import homeassistant.helpers.entity_registry as er
from . import BangOlufsenConfigEntry
from .const import DOMAIN
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
data: dict = {
"config_entry": config_entry.as_dict(),
"websocket_connected": config_entry.runtime_data.client.websocket_connected,
}
if TYPE_CHECKING:
assert config_entry.unique_id
# Add media_player entity's state
entity_registry = er.async_get(hass)
if entity_id := entity_registry.async_get_entity_id(
MEDIA_PLAYER_DOMAIN, DOMAIN, config_entry.unique_id
):
if state := hass.states.get(entity_id):
state_dict = dict(state.as_dict())
# Remove context as it is not relevant
state_dict.pop("context")
data["media_player"] = state_dict
return data

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/bang_olufsen", "documentation": "https://www.home-assistant.io/integrations/bang_olufsen",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["mozart-api==4.1.1.116.0"], "requirements": ["mozart-api==4.1.1.116.3"],
"zeroconf": ["_bangolufsen._tcp.local."] "zeroconf": ["_bangolufsen._tcp.local."]
} }

View File

@ -86,6 +86,8 @@ from .const import (
from .entity import BangOlufsenEntity from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0
SCAN_INTERVAL = timedelta(seconds=30) SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -180,7 +182,6 @@ async def async_setup_entry(
class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity): class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player.""" """Representation of a media player."""
_attr_icon = "mdi:speaker-wireless"
_attr_name = None _attr_name = None
_attr_device_class = MediaPlayerDeviceClass.SPEAKER _attr_device_class = MediaPlayerDeviceClass.SPEAKER

View File

@ -11,7 +11,7 @@
"invalid_ip": "Invalid IPv4 address" "invalid_ip": "Invalid IPv4 address"
}, },
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::single_instance_allowed%]", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]" "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]"
}, },
"flow_title": "{name}", "flow_title": "{name}",

View File

@ -15,7 +15,7 @@ from mozart_api.models import (
VolumeState, VolumeState,
WebsocketNotificationTag, WebsocketNotificationTag,
) )
from mozart_api.mozart_client import MozartClient from mozart_api.mozart_client import BaseWebSocketResponse, MozartClient
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -202,12 +202,13 @@ class BangOlufsenWebsocket(BangOlufsenBase):
sw_version=software_status.software_version, sw_version=software_status.software_version,
) )
def on_all_notifications_raw(self, notification: dict) -> None: def on_all_notifications_raw(self, notification: BaseWebSocketResponse) -> None:
"""Receive all notifications.""" """Receive all notifications."""
debug_notification = {
"device_id": self._device.id,
"serial_number": int(self._unique_id),
**notification,
}
# Add the device_id and serial_number to the notification _LOGGER.debug("%s", debug_notification)
notification["device_id"] = self._device.id self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)
notification["serial_number"] = int(self._unique_id)
_LOGGER.debug("%s", notification)
self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, notification)

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/bbox", "documentation": "https://www.home-assistant.io/integrations/bbox",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pybbox"], "loggers": ["pybbox"],
"quality_scale": "legacy",
"requirements": ["pybbox==0.0.5-alpha"] "requirements": ["pybbox==0.0.5-alpha"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/beewi_smartclim", "documentation": "https://www.home-assistant.io/integrations/beewi_smartclim",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["beewi_smartclim"], "loggers": ["beewi_smartclim"],
"quality_scale": "legacy",
"requirements": ["beewi-smartclim==0.0.10"] "requirements": ["beewi-smartclim==0.0.10"]
} }

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from datetime import timedelta from datetime import timedelta
from enum import StrEnum from enum import StrEnum
from functools import partial
import logging import logging
from typing import Literal, final from typing import Literal, final
@ -16,12 +15,6 @@ from homeassistant.const import STATE_OFF, STATE_ON, EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.deprecation import (
DeprecatedConstantEnum,
all_with_deprecated_constants,
check_if_deprecated_constant,
dir_with_deprecated_constants,
)
from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
@ -126,94 +119,7 @@ class BinarySensorDeviceClass(StrEnum):
DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass)) DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(BinarySensorDeviceClass))
# DEVICE_CLASS* below are deprecated as of 2021.12
# use the BinarySensorDeviceClass enum instead.
DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass] DEVICE_CLASSES = [cls.value for cls in BinarySensorDeviceClass]
_DEPRECATED_DEVICE_CLASS_BATTERY = DeprecatedConstantEnum(
BinarySensorDeviceClass.BATTERY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_BATTERY_CHARGING = DeprecatedConstantEnum(
BinarySensorDeviceClass.BATTERY_CHARGING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_CO = DeprecatedConstantEnum(
BinarySensorDeviceClass.CO, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_COLD = DeprecatedConstantEnum(
BinarySensorDeviceClass.COLD, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_CONNECTIVITY = DeprecatedConstantEnum(
BinarySensorDeviceClass.CONNECTIVITY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_DOOR = DeprecatedConstantEnum(
BinarySensorDeviceClass.DOOR, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_GARAGE_DOOR = DeprecatedConstantEnum(
BinarySensorDeviceClass.GARAGE_DOOR, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_GAS = DeprecatedConstantEnum(
BinarySensorDeviceClass.GAS, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_HEAT = DeprecatedConstantEnum(
BinarySensorDeviceClass.HEAT, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_LIGHT = DeprecatedConstantEnum(
BinarySensorDeviceClass.LIGHT, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_LOCK = DeprecatedConstantEnum(
BinarySensorDeviceClass.LOCK, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOISTURE = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOISTURE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOTION = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOTION, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_MOVING = DeprecatedConstantEnum(
BinarySensorDeviceClass.MOVING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_OCCUPANCY = DeprecatedConstantEnum(
BinarySensorDeviceClass.OCCUPANCY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_OPENING = DeprecatedConstantEnum(
BinarySensorDeviceClass.OPENING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PLUG = DeprecatedConstantEnum(
BinarySensorDeviceClass.PLUG, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_POWER = DeprecatedConstantEnum(
BinarySensorDeviceClass.POWER, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PRESENCE = DeprecatedConstantEnum(
BinarySensorDeviceClass.PRESENCE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_PROBLEM = DeprecatedConstantEnum(
BinarySensorDeviceClass.PROBLEM, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_RUNNING = DeprecatedConstantEnum(
BinarySensorDeviceClass.RUNNING, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SAFETY = DeprecatedConstantEnum(
BinarySensorDeviceClass.SAFETY, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SMOKE = DeprecatedConstantEnum(
BinarySensorDeviceClass.SMOKE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_SOUND = DeprecatedConstantEnum(
BinarySensorDeviceClass.SOUND, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_TAMPER = DeprecatedConstantEnum(
BinarySensorDeviceClass.TAMPER, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_UPDATE = DeprecatedConstantEnum(
BinarySensorDeviceClass.UPDATE, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_VIBRATION = DeprecatedConstantEnum(
BinarySensorDeviceClass.VIBRATION, "2025.1"
)
_DEPRECATED_DEVICE_CLASS_WINDOW = DeprecatedConstantEnum(
BinarySensorDeviceClass.WINDOW, "2025.1"
)
# mypy: disallow-any-generics # mypy: disallow-any-generics
@ -294,11 +200,3 @@ class BinarySensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
if (is_on := self.is_on) is None: if (is_on := self.is_on) is None:
return None return None
return STATE_ON if is_on else STATE_OFF return STATE_ON if is_on else STATE_OFF
# These can be removed if no deprecated constant are in this module anymore
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
__dir__ = partial(
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
)
__all__ = all_with_deprecated_constants(globals())

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/bitcoin", "documentation": "https://www.home-assistant.io/integrations/bitcoin",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["blockchain"], "loggers": ["blockchain"],
"quality_scale": "legacy",
"requirements": ["blockchain==1.4.4"] "requirements": ["blockchain==1.4.4"]
} }

View File

@ -5,5 +5,6 @@
"documentation": "https://www.home-assistant.io/integrations/bizkaibus", "documentation": "https://www.home-assistant.io/integrations/bizkaibus",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["bizkaibus"], "loggers": ["bizkaibus"],
"quality_scale": "legacy",
"requirements": ["bizkaibus==0.1.1"] "requirements": ["bizkaibus==0.1.1"]
} }

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