Compare commits

..

1 Commits

Author SHA1 Message Date
Ludovic BOUÉ
6d21f708fa Add setback attributes and properties to ClimateEntity 2025-12-03 14:24:28 +00:00
1050 changed files with 10339 additions and 37801 deletions

View File

@@ -13,7 +13,6 @@ core: &core
# Our base platforms, that are used by other integrations # Our base platforms, that are used by other integrations
base_platforms: &base_platforms base_platforms: &base_platforms
- homeassistant/components/ai_task/**
- homeassistant/components/air_quality/** - homeassistant/components/air_quality/**
- homeassistant/components/alarm_control_panel/** - homeassistant/components/alarm_control_panel/**
- homeassistant/components/assist_satellite/** - homeassistant/components/assist_satellite/**

View File

@@ -27,6 +27,7 @@
"charliermarsh.ruff", "charliermarsh.ruff",
"ms-python.pylint", "ms-python.pylint",
"ms-python.vscode-pylance", "ms-python.vscode-pylance",
"visualstudioexptteam.vscodeintellicode",
"redhat.vscode-yaml", "redhat.vscode-yaml",
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"GitHub.vscode-pull-request-github", "GitHub.vscode-pull-request-github",

View File

@@ -15,7 +15,7 @@ env:
UV_HTTP_TIMEOUT: 60 UV_HTTP_TIMEOUT: 60
UV_SYSTEM_PYTHON: "true" UV_SYSTEM_PYTHON: "true"
# Base image version from https://github.com/home-assistant/docker # Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2025.12.0" BASE_IMAGE_VERSION: "2025.11.3"
ARCHITECTURES: '["amd64", "aarch64"]' ARCHITECTURES: '["amd64", "aarch64"]'
jobs: jobs:
@@ -70,7 +70,7 @@ jobs:
run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T -
- name: Upload translations - name: Upload translations
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: translations name: translations
path: translations.tar.gz path: translations.tar.gz
@@ -169,7 +169,7 @@ jobs:
fi fi
- name: Download translations - name: Download translations
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: translations name: translations
@@ -416,19 +416,9 @@ jobs:
ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]') ARCHS=$(echo '${{ needs.init.outputs.architectures }}' | jq -r '.[]')
for arch in $ARCHS; do for arch in $ARCHS; do
echo "Copying ${arch} image to DockerHub..." echo "Copying ${arch} image to DockerHub..."
for attempt in 1 2 3; do docker buildx imagetools create \
if docker buildx imagetools create \ --tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \
--tag "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" \ "ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
"ghcr.io/home-assistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"; then
break
fi
echo "Attempt ${attempt} failed, retrying in 10 seconds..."
sleep 10
if [ "${attempt}" -eq 3 ]; then
echo "Failed after 3 attempts"
exit 1
fi
done
cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}" cosign sign --yes "docker.io/homeassistant/${arch}-homeassistant:${{ needs.init.outputs.version }}"
done done
@@ -482,7 +472,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Download translations - name: Download translations
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: translations name: translations

View File

@@ -41,8 +41,8 @@ env:
UV_CACHE_VERSION: 1 UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1 MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2026.1" HA_SHORT_VERSION: "2026.1"
DEFAULT_PYTHON: "3.13.11" DEFAULT_PYTHON: "3.13.9"
ALL_PYTHON_VERSIONS: "['3.13.11', '3.14.2']" ALL_PYTHON_VERSIONS: "['3.13.9', '3.14.0']"
# 10.3 is the oldest supported version # 10.3 is the oldest supported version
# - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022) # - 10.3.32 is the version currently shipped with Synology (as of 17 Feb 2022)
# 10.6 is the current long-term-support # 10.6 is the current long-term-support
@@ -263,7 +263,7 @@ jobs:
check-latest: true check-latest: true
- name: Restore base Python virtual environment - name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: &actions-cache actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: &actions-cache actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
key: &key-pre-commit-venv >- key: &key-pre-commit-venv >-
@@ -304,7 +304,7 @@ jobs:
- &cache-restore-pre-commit-venv - &cache-restore-pre-commit-venv
name: Restore base Python virtual environment name: Restore base Python virtual environment
id: cache-venv id: cache-venv
uses: &actions-cache-restore actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: &actions-cache-restore actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
fail-on-cache-miss: true fail-on-cache-miss: true
@@ -511,7 +511,7 @@ jobs:
fi fi
- name: Save apt cache - name: Save apt cache
if: steps.cache-apt-check.outputs.cache-hit != 'true' if: steps.cache-apt-check.outputs.cache-hit != 'true'
uses: &actions-cache-save actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: &actions-cache-save actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: *path-apt-cache path: *path-apt-cache
key: *key-apt-cache key: *key-apt-cache
@@ -534,7 +534,7 @@ jobs:
python --version python --version
uv pip freeze >> pip_freeze.txt uv pip freeze >> pip_freeze.txt
- name: Upload pip_freeze artifact - name: Upload pip_freeze artifact
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: pip-freeze-${{ matrix.python-version }} name: pip-freeze-${{ matrix.python-version }}
path: pip_freeze.txt path: pip_freeze.txt
@@ -864,7 +864,7 @@ jobs:
run: | run: |
echo "::add-matcher::.github/workflows/matchers/pytest-slow.json" echo "::add-matcher::.github/workflows/matchers/pytest-slow.json"
- name: Download pytest_buckets - name: Download pytest_buckets
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: pytest_buckets name: pytest_buckets
- &compile-english-translations - &compile-english-translations
@@ -1188,7 +1188,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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
fail_ci_if_error: true fail_ci_if_error: true
flags: full-suite flags: full-suite
@@ -1313,7 +1313,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@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with: with:
languages: python languages: python
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@1b168cd39490f61582a9beae412bb7057a6b2c4e # v4.31.8 uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
with: with:
category: "/language:python" category: "/language:python"

View File

@@ -10,7 +10,7 @@ jobs:
if: github.repository_owner == 'home-assistant' if: github.repository_owner == 'home-assistant'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0 - uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
with: with:
github-token: ${{ github.token }} github-token: ${{ github.token }}
issue-inactive-days: "30" issue-inactive-days: "30"

View File

@@ -74,7 +74,7 @@ jobs:
) > .env_file ) > .env_file
- name: Upload env_file - name: Upload env_file
uses: &actions-upload-artifact actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: &actions-upload-artifact actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with: with:
name: env_file name: env_file
path: ./.env_file path: ./.env_file
@@ -119,7 +119,7 @@ jobs:
- &download-env-file - &download-env-file
name: Download env_file name: Download env_file
uses: &actions-download-artifact actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: &actions-download-artifact actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with: with:
name: env_file name: env_file

23
CODEOWNERS generated
View File

@@ -73,8 +73,6 @@ build.json @home-assistant/supervisor
/tests/components/airobot/ @mettolen /tests/components/airobot/ @mettolen
/homeassistant/components/airos/ @CoMPaTech /homeassistant/components/airos/ @CoMPaTech
/tests/components/airos/ @CoMPaTech /tests/components/airos/ @CoMPaTech
/homeassistant/components/airpatrol/ @antondalgren
/tests/components/airpatrol/ @antondalgren
/homeassistant/components/airq/ @Sibgatulin @dl2080 /homeassistant/components/airq/ @Sibgatulin @dl2080
/tests/components/airq/ @Sibgatulin @dl2080 /tests/components/airq/ @Sibgatulin @dl2080
/homeassistant/components/airthings/ @danielhiversen @LaStrada /homeassistant/components/airthings/ @danielhiversen @LaStrada
@@ -220,8 +218,8 @@ build.json @home-assistant/supervisor
/homeassistant/components/bizkaibus/ @UgaitzEtxebarria /homeassistant/components/bizkaibus/ @UgaitzEtxebarria
/homeassistant/components/blebox/ @bbx-a @swistakm /homeassistant/components/blebox/ @bbx-a @swistakm
/tests/components/blebox/ @bbx-a @swistakm /tests/components/blebox/ @bbx-a @swistakm
/homeassistant/components/blink/ @fronzbot /homeassistant/components/blink/ @fronzbot @mkmer
/tests/components/blink/ @fronzbot /tests/components/blink/ @fronzbot @mkmer
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23 /homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23 /tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
/homeassistant/components/bluemaestro/ @bdraco /homeassistant/components/bluemaestro/ @bdraco
@@ -308,8 +306,8 @@ build.json @home-assistant/supervisor
/tests/components/config/ @home-assistant/core /tests/components/config/ @home-assistant/core
/homeassistant/components/configurator/ @home-assistant/core /homeassistant/components/configurator/ @home-assistant/core
/tests/components/configurator/ @home-assistant/core /tests/components/configurator/ @home-assistant/core
/homeassistant/components/control4/ @lawtancool @davidrecordon /homeassistant/components/control4/ @lawtancool
/tests/components/control4/ @lawtancool @davidrecordon /tests/components/control4/ @lawtancool
/homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz /homeassistant/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
/tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz /tests/components/conversation/ @home-assistant/core @synesthesiam @arturpragacz
/homeassistant/components/cookidoo/ @miaucl /homeassistant/components/cookidoo/ @miaucl
@@ -420,8 +418,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/efergy/ @tkdrob /homeassistant/components/efergy/ @tkdrob
/tests/components/efergy/ @tkdrob /tests/components/efergy/ @tkdrob
/homeassistant/components/egardia/ @jeroenterheerdt /homeassistant/components/egardia/ @jeroenterheerdt
/homeassistant/components/egauge/ @neggert
/tests/components/egauge/ @neggert
/homeassistant/components/eheimdigital/ @autinerd /homeassistant/components/eheimdigital/ @autinerd
/tests/components/eheimdigital/ @autinerd /tests/components/eheimdigital/ @autinerd
/homeassistant/components/ekeybionyx/ @richardpolzer /homeassistant/components/ekeybionyx/ @richardpolzer
@@ -464,7 +460,7 @@ build.json @home-assistant/supervisor
/tests/components/enigma2/ @autinerd /tests/components/enigma2/ @autinerd
/homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac /homeassistant/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac /tests/components/enphase_envoy/ @bdraco @cgarwood @catsmanac
/homeassistant/components/entur_public_transport/ @hfurubotten @SanderBlom /homeassistant/components/entur_public_transport/ @hfurubotten
/homeassistant/components/environment_canada/ @gwww @michaeldavie /homeassistant/components/environment_canada/ @gwww @michaeldavie
/tests/components/environment_canada/ @gwww @michaeldavie /tests/components/environment_canada/ @gwww @michaeldavie
/homeassistant/components/ephember/ @ttroy50 @roberty99 /homeassistant/components/ephember/ @ttroy50 @roberty99
@@ -575,8 +571,6 @@ build.json @home-assistant/supervisor
/tests/components/generic_hygrostat/ @Shulyaka /tests/components/generic_hygrostat/ @Shulyaka
/homeassistant/components/geniushub/ @manzanotti /homeassistant/components/geniushub/ @manzanotti
/tests/components/geniushub/ @manzanotti /tests/components/geniushub/ @manzanotti
/homeassistant/components/gentex_homelink/ @niaexa @ryanjones-gentex
/tests/components/gentex_homelink/ @niaexa @ryanjones-gentex
/homeassistant/components/geo_json_events/ @exxamalte /homeassistant/components/geo_json_events/ @exxamalte
/tests/components/geo_json_events/ @exxamalte /tests/components/geo_json_events/ @exxamalte
/homeassistant/components/geo_location/ @home-assistant/core /homeassistant/components/geo_location/ @home-assistant/core
@@ -665,7 +659,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/here_travel_time/ @eifinger /homeassistant/components/here_travel_time/ @eifinger
/tests/components/here_travel_time/ @eifinger /tests/components/here_travel_time/ @eifinger
/homeassistant/components/hikvision/ @mezz64 /homeassistant/components/hikvision/ @mezz64
/tests/components/hikvision/ @mezz64
/homeassistant/components/hikvisioncam/ @fbradyirl /homeassistant/components/hikvisioncam/ @fbradyirl
/homeassistant/components/hisense_aehw4a1/ @bannhead /homeassistant/components/hisense_aehw4a1/ @bannhead
/tests/components/hisense_aehw4a1/ @bannhead /tests/components/hisense_aehw4a1/ @bannhead
@@ -1363,8 +1356,8 @@ build.json @home-assistant/supervisor
/tests/components/ring/ @sdb9696 /tests/components/ring/ @sdb9696
/homeassistant/components/risco/ @OnFreund /homeassistant/components/risco/ @OnFreund
/tests/components/risco/ @OnFreund /tests/components/risco/ @OnFreund
/homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm /homeassistant/components/rituals_perfume_genie/ @milanmeu @frenck
/tests/components/rituals_perfume_genie/ @milanmeu @frenck @quebulm /tests/components/rituals_perfume_genie/ @milanmeu @frenck
/homeassistant/components/rmvtransport/ @cgtobi /homeassistant/components/rmvtransport/ @cgtobi
/tests/components/rmvtransport/ @cgtobi /tests/components/rmvtransport/ @cgtobi
/homeassistant/components/roborock/ @Lash-L @allenporter /homeassistant/components/roborock/ @Lash-L @allenporter
@@ -1810,8 +1803,6 @@ build.json @home-assistant/supervisor
/tests/components/weatherflow_cloud/ @jeeftor /tests/components/weatherflow_cloud/ @jeeftor
/homeassistant/components/weatherkit/ @tjhorner /homeassistant/components/weatherkit/ @tjhorner
/tests/components/weatherkit/ @tjhorner /tests/components/weatherkit/ @tjhorner
/homeassistant/components/web_rtc/ @home-assistant/core
/tests/components/web_rtc/ @home-assistant/core
/homeassistant/components/webdav/ @jpbede /homeassistant/components/webdav/ @jpbede
/tests/components/webdav/ @jpbede /tests/components/webdav/ @jpbede
/homeassistant/components/webhook/ @home-assistant/core /homeassistant/components/webhook/ @home-assistant/core

2
Dockerfile generated
View File

@@ -30,7 +30,7 @@ RUN \
# Verify go2rtc can be executed # Verify go2rtc can be executed
go2rtc --version \ go2rtc --version \
# Install uv # Install uv
&& pip3 install uv==0.9.17 && pip3 install uv==0.9.6
WORKDIR /usr/src WORKDIR /usr/src

View File

@@ -624,16 +624,13 @@ async def async_enable_logging(
if log_file is None: if log_file is None:
default_log_path = hass.config.path(ERROR_LOG_FILENAME) default_log_path = hass.config.path(ERROR_LOG_FILENAME)
if "SUPERVISOR" in os.environ and "HA_DUPLICATE_LOG_FILE" not in os.environ: if "SUPERVISOR" in os.environ:
_LOGGER.info("Running in Supervisor, not logging to file")
# Rename the default log file if it exists, since previous versions created # Rename the default log file if it exists, since previous versions created
# it even on Supervisor # it even on Supervisor
def rename_old_file() -> None: if os.path.isfile(default_log_path):
"""Rename old log file in executor.""" with contextlib.suppress(OSError):
if os.path.isfile(default_log_path): os.rename(default_log_path, f"{default_log_path}.old")
with contextlib.suppress(OSError):
os.rename(default_log_path, f"{default_log_path}.old")
await hass.async_add_executor_job(rename_old_file)
err_log_path = None err_log_path = None
else: else:
err_log_path = default_log_path err_log_path = default_log_path

View File

@@ -9,9 +9,8 @@ from actron_neo_api import (
from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import _LOGGER, DOMAIN from .const import _LOGGER
from .coordinator import ( from .coordinator import (
ActronAirConfigEntry, ActronAirConfigEntry,
ActronAirRuntimeData, ActronAirRuntimeData,
@@ -30,13 +29,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
try: try:
systems = await api.get_ac_systems() systems = await api.get_ac_systems()
await api.update_status() await api.update_status()
except ActronAirAuthError as err: except ActronAirAuthError:
raise ConfigEntryAuthFailed( _LOGGER.error("Authentication error while setting up Actron Air integration")
translation_domain=DOMAIN, raise
translation_key="auth_error",
) from err
except ActronAirAPIError as err: except ActronAirAPIError as err:
raise ConfigEntryNotReady from err _LOGGER.error("API error while setting up Actron Air integration: %s", err)
raise
system_coordinators: dict[str, ActronAirSystemCoordinator] = {} system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems: for system in systems:

View File

@@ -1,12 +1,11 @@
"""Setup config flow for Actron Air integration.""" """Setup config flow for Actron Air integration."""
import asyncio import asyncio
from collections.abc import Mapping
from typing import Any from typing import Any
from actron_neo_api import ActronAirAPI, ActronAirAuthError from actron_neo_api import ActronAirAPI, ActronAirAuthError
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN from homeassistant.const import CONF_API_TOKEN
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@@ -96,16 +95,8 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
unique_id = str(user_data["id"]) unique_id = str(user_data["id"])
await self.async_set_unique_id(unique_id) await self.async_set_unique_id(unique_id)
# Check if this is a reauth flow
if self.source == SOURCE_REAUTH:
self._abort_if_unique_id_mismatch(reason="wrong_account")
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates={CONF_API_TOKEN: self._api.refresh_token_value},
)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=user_data["email"], title=user_data["email"],
data={CONF_API_TOKEN: self._api.refresh_token_value}, data={CONF_API_TOKEN: self._api.refresh_token_value},
@@ -123,21 +114,6 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
del self.login_task del self.login_task
return await self.async_step_user() return await self.async_step_user()
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication request."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauth dialog."""
if user_input is not None:
return await self.async_step_user()
return self.async_show_form(step_id="reauth_confirm")
async def async_step_connection_error( async def async_step_connection_error(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:

View File

@@ -5,23 +5,16 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from actron_neo_api import ( from actron_neo_api import ActronAirACSystem, ActronAirAPI, ActronAirStatus
ActronAirACSystem,
ActronAirAPI,
ActronAirAuthError,
ActronAirStatus,
)
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 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .const import _LOGGER, DOMAIN from .const import _LOGGER
SCAN_INTERVAL = timedelta(seconds=30) STALE_DEVICE_TIMEOUT = timedelta(hours=24)
STALE_DEVICE_TIMEOUT = timedelta(minutes=5)
ERROR_NO_SYSTEMS_FOUND = "no_systems_found" ERROR_NO_SYSTEMS_FOUND = "no_systems_found"
ERROR_UNKNOWN = "unknown_error" ERROR_UNKNOWN = "unknown_error"
@@ -36,6 +29,9 @@ class ActronAirRuntimeData:
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData] type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
AUTH_ERROR_THRESHOLD = 3
SCAN_INTERVAL = timedelta(seconds=30)
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]): class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
"""System coordinator for Actron Air integration.""" """System coordinator for Actron Air integration."""
@@ -63,14 +59,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
async def _async_update_data(self) -> ActronAirStatus: async def _async_update_data(self) -> ActronAirStatus:
"""Fetch updates and merge incremental changes into the full state.""" """Fetch updates and merge incremental changes into the full state."""
try: await self.api.update_status()
await self.api.update_status()
except ActronAirAuthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="auth_error",
) from err
self.status = self.api.state_manager.get_status(self.serial_number) self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow() self.last_seen = dt_util.utcnow()
return self.status return self.status

View File

@@ -10,8 +10,7 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/actron_air", "documentation": "https://www.home-assistant.io/integrations/actron_air",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["actron-neo-api==0.2.0"] "requirements": ["actron-neo-api==0.1.87"]
} }

View File

@@ -36,7 +36,7 @@ rules:
integration-owner: done integration-owner: done
log-when-unavailable: done log-when-unavailable: done
parallel-updates: done parallel-updates: done
reauthentication-flow: done reauthentication-flow: todo
test-coverage: todo test-coverage: todo
# Gold # Gold

View File

@@ -2,12 +2,10 @@
"config": { "config": {
"abort": { "abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"oauth2_error": "Failed to start authentication flow", "oauth2_error": "Failed to start OAuth2 flow"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"wrong_account": "You must reauthenticate with the same Actron Air account that was originally configured."
}, },
"error": { "error": {
"oauth2_error": "Failed to start authentication flow. Please try again later." "oauth2_error": "Failed to start OAuth2 flow. Please try again later."
}, },
"progress": { "progress": {
"wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes." "wait_for_authorization": "To authenticate, open the following URL and login at Actron Air:\n{verification_uri}\nIf the code is not automatically copied, paste the following code to authorize the integration:\n\n```{user_code}```\n\n\nThe login attempt will time out after {expires_minutes} minutes."
@@ -18,23 +16,14 @@
"description": "Failed to connect to Actron Air. Please check your internet connection and try again.", "description": "Failed to connect to Actron Air. Please check your internet connection and try again.",
"title": "Connection error" "title": "Connection error"
}, },
"reauth_confirm": {
"description": "Your Actron Air authentication has expired. Select continue to reauthenticate with your Actron Air account. You will be prompted to log in again to restore the connection.",
"title": "Authentication expired"
},
"timeout": { "timeout": {
"data": {}, "data": {},
"description": "The authentication process timed out. Please try again.", "description": "The authorization process timed out. Please try again.",
"title": "Authentication timeout" "title": "Authorization timeout"
}, },
"user": { "user": {
"title": "Actron Air Authentication" "title": "Actron Air OAuth2 Authorization"
} }
} }
},
"exceptions": {
"auth_error": {
"message": "Authentication failed, please reauthenticate"
}
} }
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@Bre77"], "codeowners": ["@Bre77"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/advantage_air", "documentation": "https://www.home-assistant.io/integrations/advantage_air",
"integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["advantage_air"], "loggers": ["advantage_air"],
"requirements": ["advantage-air==0.4.4"] "requirements": ["advantage-air==0.4.4"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@Noltari"], "codeowners": ["@Noltari"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aemet", "documentation": "https://www.home-assistant.io/integrations/aemet",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aemet_opendata"], "loggers": ["aemet_opendata"],
"requirements": ["AEMET-OpenData==0.6.4"] "requirements": ["AEMET-OpenData==0.6.4"]

View File

@@ -4,7 +4,6 @@
"codeowners": [], "codeowners": [],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aftership", "documentation": "https://www.home-assistant.io/integrations/aftership",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["pyaftership==21.11.0"] "requirements": ["pyaftership==21.11.0"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@ispysoftware"], "codeowners": ["@ispysoftware"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/agent_dvr", "documentation": "https://www.home-assistant.io/integrations/agent_dvr",
"integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["agent"], "loggers": ["agent"],
"requirements": ["agent-py==0.0.24"] "requirements": ["agent-py==0.0.24"]

View File

@@ -101,8 +101,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Schema({str: STRUCTURE_FIELD_SCHEMA}), vol.Schema({str: STRUCTURE_FIELD_SCHEMA}),
_validate_structure_fields, _validate_structure_fields,
), ),
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector( vol.Optional(ATTR_ATTACHMENTS): vol.All(
{"accept": ["*/*"], "multiple": True} cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
), ),
} }
), ),
@@ -118,8 +118,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
vol.Required(ATTR_TASK_NAME): cv.string, vol.Required(ATTR_TASK_NAME): cv.string,
vol.Optional(ATTR_ENTITY_ID): cv.entity_id, vol.Optional(ATTR_ENTITY_ID): cv.entity_id,
vol.Required(ATTR_INSTRUCTIONS): cv.string, vol.Required(ATTR_INSTRUCTIONS): cv.string,
vol.Optional(ATTR_ATTACHMENTS): selector.MediaSelector( vol.Optional(ATTR_ATTACHMENTS): vol.All(
{"accept": ["*/*"], "multiple": True} cv.ensure_list, [selector.MediaSelector({"accept": ["*/*"]})]
), ),
} }
), ),

View File

@@ -4,7 +4,6 @@
"codeowners": ["@asymworks"], "codeowners": ["@asymworks"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airnow", "documentation": "https://www.home-assistant.io/integrations/airnow",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyairnow"], "loggers": ["pyairnow"],
"requirements": ["pyairnow==1.3.1"] "requirements": ["pyairnow==1.3.1"]

View File

@@ -7,7 +7,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator from .coordinator import AirobotConfigEntry, AirobotDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.CLIMATE]
async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: AirobotConfigEntry) -> bool:

View File

@@ -1,38 +0,0 @@
"""Diagnostics support for Airobot."""
from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .coordinator import AirobotConfigEntry
TO_REDACT_CONFIG = [CONF_HOST, CONF_MAC, CONF_PASSWORD, CONF_USERNAME]
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: AirobotConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator = entry.runtime_data
# Build device capabilities info
device_capabilities = None
if coordinator.data:
device_capabilities = {
"has_floor_sensor": coordinator.data.status.has_floor_sensor,
"has_co2_sensor": coordinator.data.status.has_co2_sensor,
"hw_version": coordinator.data.status.hw_version,
"fw_version": coordinator.data.status.fw_version,
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT_CONFIG),
"device_capabilities": device_capabilities,
"status": asdict(coordinator.data.status) if coordinator.data else None,
"settings": asdict(coordinator.data.settings) if coordinator.data else None,
}

View File

@@ -39,12 +39,12 @@ rules:
# Gold # Gold
devices: done devices: done
diagnostics: done diagnostics: todo
discovery-update-info: done discovery-update-info: done
discovery: done discovery: done
docs-data-update: done docs-data-update: done
docs-examples: todo docs-examples: todo
docs-known-limitations: done docs-known-limitations: todo
docs-supported-devices: done docs-supported-devices: done
docs-supported-functions: done docs-supported-functions: done
docs-troubleshooting: done docs-troubleshooting: done
@@ -54,8 +54,8 @@ rules:
comment: Single device integration, no dynamic device discovery needed. comment: Single device integration, no dynamic device discovery needed.
entity-category: done entity-category: done
entity-device-class: done entity-device-class: done
entity-disabled-by-default: done entity-disabled-by-default: todo
entity-translations: done entity-translations: todo
exception-translations: done exception-translations: done
icon-translations: todo icon-translations: todo
reconfiguration-flow: todo reconfiguration-flow: todo

View File

@@ -1,150 +0,0 @@
"""Sensor platform for Airobot thermostat."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from pyairobotrest.models import ThermostatStatus
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
EntityCategory,
UnitOfTemperature,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from homeassistant.util.variance import ignore_variance
from . import AirobotConfigEntry
from .entity import AirobotEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class AirobotSensorEntityDescription(SensorEntityDescription):
"""Describes Airobot sensor entity."""
value_fn: Callable[[ThermostatStatus], StateType | datetime]
supported_fn: Callable[[ThermostatStatus], bool] = lambda _: True
uptime_to_stable_datetime = ignore_variance(
lambda value: utcnow().replace(microsecond=0) - timedelta(seconds=value),
timedelta(minutes=2),
)
SENSOR_TYPES: tuple[AirobotSensorEntityDescription, ...] = (
AirobotSensorEntityDescription(
key="air_temperature",
translation_key="air_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_air,
),
AirobotSensorEntityDescription(
key="humidity",
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.hum_air,
),
AirobotSensorEntityDescription(
key="floor_temperature",
translation_key="floor_temperature",
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.temp_floor,
supported_fn=lambda status: status.has_floor_sensor,
),
AirobotSensorEntityDescription(
key="co2",
device_class=SensorDeviceClass.CO2,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.co2,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="air_quality_index",
device_class=SensorDeviceClass.AQI,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda status: status.aqi,
supported_fn=lambda status: status.has_co2_sensor,
),
AirobotSensorEntityDescription(
key="heating_uptime",
translation_key="heating_uptime",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=SensorStateClass.TOTAL_INCREASING,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.heating_uptime,
entity_registry_enabled_default=False,
),
AirobotSensorEntityDescription(
key="errors",
translation_key="errors",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: status.errors,
),
AirobotSensorEntityDescription(
key="device_uptime",
translation_key="device_uptime",
device_class=SensorDeviceClass.TIMESTAMP,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda status: uptime_to_stable_datetime(status.device_uptime),
entity_registry_enabled_default=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: AirobotConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Airobot sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
AirobotSensor(coordinator, description)
for description in SENSOR_TYPES
if description.supported_fn(coordinator.data.status)
)
class AirobotSensor(AirobotEntity, SensorEntity):
"""Representation of an Airobot sensor."""
entity_description: AirobotSensorEntityDescription
def __init__(
self,
coordinator,
description: AirobotSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{coordinator.data.status.device_id}_{description.key}"
@property
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.coordinator.data.status)

View File

@@ -43,25 +43,6 @@
} }
} }
}, },
"entity": {
"sensor": {
"air_temperature": {
"name": "Air temperature"
},
"device_uptime": {
"name": "Device uptime"
},
"errors": {
"name": "Error count"
},
"floor_temperature": {
"name": "Floor temperature"
},
"heating_uptime": {
"name": "Heating uptime"
}
}
},
"exceptions": { "exceptions": {
"authentication_failed": { "authentication_failed": {
"message": "Authentication failed, please reauthenticate." "message": "Authentication failed, please reauthenticate."

View File

@@ -1,24 +0,0 @@
"""The AirPatrol integration."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from .const import PLATFORMS
from .coordinator import AirPatrolConfigEntry, AirPatrolDataUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
"""Set up AirPatrol from a config entry."""
coordinator = AirPatrolDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: AirPatrolConfigEntry) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,208 +0,0 @@
"""Climate platform for AirPatrol integration."""
from __future__ import annotations
from typing import Any
from homeassistant.components.climate import (
FAN_AUTO,
FAN_HIGH,
FAN_LOW,
SWING_OFF,
SWING_ON,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AirPatrolConfigEntry
from .coordinator import AirPatrolDataUpdateCoordinator
from .entity import AirPatrolEntity
PARALLEL_UPDATES = 0
AP_TO_HA_HVAC_MODES = {
"heat": HVACMode.HEAT,
"cool": HVACMode.COOL,
"off": HVACMode.OFF,
}
HA_TO_AP_HVAC_MODES = {value: key for key, value in AP_TO_HA_HVAC_MODES.items()}
AP_TO_HA_FAN_MODES = {
"min": FAN_LOW,
"max": FAN_HIGH,
"auto": FAN_AUTO,
}
HA_TO_AP_FAN_MODES = {value: key for key, value in AP_TO_HA_FAN_MODES.items()}
AP_TO_HA_SWING_MODES = {
"on": SWING_ON,
"off": SWING_OFF,
}
HA_TO_AP_SWING_MODES = {value: key for key, value in AP_TO_HA_SWING_MODES.items()}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: AirPatrolConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up AirPatrol climate entities."""
coordinator = config_entry.runtime_data
units = coordinator.data
async_add_entities(
AirPatrolClimate(coordinator, unit_id)
for unit_id, unit in units.items()
if "climate" in unit
)
class AirPatrolClimate(AirPatrolEntity, ClimateEntity):
"""AirPatrol climate entity."""
_attr_name = None
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF]
_attr_fan_modes = [FAN_LOW, FAN_HIGH, FAN_AUTO]
_attr_swing_modes = [SWING_ON, SWING_OFF]
_attr_min_temp = 16.0
_attr_max_temp = 30.0
def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
) -> None:
"""Initialize the climate entity."""
super().__init__(coordinator, unit_id)
self._attr_unique_id = f"{coordinator.config_entry.unique_id}-{unit_id}"
@property
def climate_data(self) -> dict[str, Any]:
"""Return the climate data."""
return self.device_data.get("climate") or {}
@property
def params(self) -> dict[str, Any]:
"""Return the current parameters for the climate entity."""
return self.climate_data.get("ParametersData") or {}
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and bool(self.climate_data)
@property
def current_humidity(self) -> float | None:
"""Return the current humidity."""
if humidity := self.climate_data.get("RoomHumidity"):
return float(humidity)
return None
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if temp := self.climate_data.get("RoomTemp"):
return float(temp)
return None
@property
def target_temperature(self) -> float | None:
"""Return the target temperature."""
if temp := self.params.get("PumpTemp"):
return float(temp)
return None
@property
def hvac_mode(self) -> HVACMode | None:
"""Return the current HVAC mode."""
pump_power = self.params.get("PumpPower")
pump_mode = self.params.get("PumpMode")
if pump_power and pump_power == "on" and pump_mode:
return AP_TO_HA_HVAC_MODES.get(pump_mode)
return HVACMode.OFF
@property
def fan_mode(self) -> str | None:
"""Return the current fan mode."""
fan_speed = self.params.get("FanSpeed")
if fan_speed:
return AP_TO_HA_FAN_MODES.get(fan_speed)
return None
@property
def swing_mode(self) -> str | None:
"""Return the current swing mode."""
swing = self.params.get("Swing")
if swing:
return AP_TO_HA_SWING_MODES.get(swing)
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
params = self.params.copy()
if ATTR_TEMPERATURE in kwargs:
temp = kwargs[ATTR_TEMPERATURE]
params["PumpTemp"] = f"{temp:.3f}"
await self._async_set_params(params)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set new target hvac mode."""
params = self.params.copy()
if hvac_mode == HVACMode.OFF:
params["PumpPower"] = "off"
else:
params["PumpPower"] = "on"
params["PumpMode"] = HA_TO_AP_HVAC_MODES.get(hvac_mode)
await self._async_set_params(params)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set new target fan mode."""
params = self.params.copy()
params["FanSpeed"] = HA_TO_AP_FAN_MODES.get(fan_mode)
await self._async_set_params(params)
async def async_set_swing_mode(self, swing_mode: str) -> None:
"""Set new target swing mode."""
params = self.params.copy()
params["Swing"] = HA_TO_AP_SWING_MODES.get(swing_mode)
await self._async_set_params(params)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
params = self.params.copy()
if mode := AP_TO_HA_HVAC_MODES.get(params["PumpMode"]):
await self.async_set_hvac_mode(mode)
async def async_turn_off(self) -> None:
"""Turn the entity off."""
await self.async_set_hvac_mode(HVACMode.OFF)
async def _async_set_params(self, params: dict[str, Any]) -> None:
"""Set the unit to dry mode."""
new_climate_data = self.climate_data.copy()
new_climate_data["ParametersData"] = params
await self.coordinator.api.set_unit_climate_data(
self._unit_id, new_climate_data
)
await self.coordinator.async_request_refresh()

View File

@@ -1,111 +0,0 @@
"""Config flow for the AirPatrol integration."""
from __future__ import annotations
from collections.abc import Mapping
from typing import Any
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DOMAIN
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_EMAIL): TextSelector(
TextSelectorConfig(
type=TextSelectorType.EMAIL,
autocomplete="email",
)
),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(
type=TextSelectorType.PASSWORD,
autocomplete="current-password",
)
),
}
)
async def validate_api(
hass: HomeAssistant, user_input: dict[str, str]
) -> tuple[str | None, str | None, dict[str, str]]:
"""Validate the API connection."""
errors: dict[str, str] = {}
session = async_get_clientsession(hass)
access_token = None
unique_id = None
try:
api = await AirPatrolAPI.authenticate(
session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
)
except AirPatrolAuthenticationError:
errors["base"] = "invalid_auth"
except AirPatrolError:
errors["base"] = "cannot_connect"
else:
access_token = api.get_access_token()
unique_id = api.get_unique_id()
return (access_token, unique_id, errors)
class AirPatrolConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for AirPatrol."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
access_token, unique_id, errors = await validate_api(self.hass, user_input)
if access_token and unique_id:
user_input[CONF_ACCESS_TOKEN] = access_token
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
return self.async_create_entry(
title=user_input[CONF_EMAIL], data=user_input
)
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauthentication with new credentials."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reauthentication confirmation."""
errors: dict[str, str] = {}
if user_input:
access_token, unique_id, errors = await validate_api(self.hass, user_input)
if access_token and unique_id:
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_mismatch()
user_input[CONF_ACCESS_TOKEN] = access_token
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm", data_schema=DATA_SCHEMA, errors=errors
)

View File

@@ -1,16 +0,0 @@
"""Constants for the AirPatrol integration."""
from datetime import timedelta
import logging
from airpatrol.api import AirPatrolAuthenticationError, AirPatrolError
from homeassistant.const import Platform
DOMAIN = "airpatrol"
LOGGER = logging.getLogger(__package__)
PLATFORMS = [Platform.CLIMATE]
SCAN_INTERVAL = timedelta(minutes=1)
AIRPATROL_ERRORS = (AirPatrolAuthenticationError, AirPatrolError)

View File

@@ -1,100 +0,0 @@
"""Data update coordinator for AirPatrol."""
from __future__ import annotations
from typing import Any
from airpatrol.api import AirPatrolAPI, AirPatrolAuthenticationError, AirPatrolError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
type AirPatrolConfigEntry = ConfigEntry[AirPatrolDataUpdateCoordinator]
class AirPatrolDataUpdateCoordinator(DataUpdateCoordinator[dict[str, dict[str, Any]]]):
"""Class to manage fetching AirPatrol data."""
config_entry: AirPatrolConfigEntry
api: AirPatrolAPI
def __init__(self, hass: HomeAssistant, config_entry: AirPatrolConfigEntry) -> None:
"""Initialize."""
super().__init__(
hass,
LOGGER,
name=f"{DOMAIN.capitalize()} {config_entry.title}",
update_interval=SCAN_INTERVAL,
config_entry=config_entry,
)
async def _async_setup(self) -> None:
try:
await self._setup_client()
except AirPatrolError as api_err:
raise UpdateFailed(
f"Error communicating with AirPatrol API: {api_err}"
) from api_err
async def _async_update_data(self) -> dict[str, dict[str, Any]]:
"""Update unit data from AirPatrol API."""
return {unit_data["unit_id"]: unit_data for unit_data in await self._get_data()}
async def _get_data(self, retry: bool = False) -> list[dict[str, Any]]:
"""Fetch data from API."""
try:
return await self.api.get_data()
except AirPatrolAuthenticationError as auth_err:
if retry:
raise ConfigEntryAuthFailed(
"Authentication with AirPatrol failed"
) from auth_err
await self._update_token()
return await self._get_data(retry=True)
except AirPatrolError as err:
raise UpdateFailed(
f"Error communicating with AirPatrol API: {err}"
) from err
async def _update_token(self) -> None:
"""Refresh the AirPatrol API client and update the access token."""
session = async_get_clientsession(self.hass)
try:
self.api = await AirPatrolAPI.authenticate(
session,
self.config_entry.data[CONF_EMAIL],
self.config_entry.data[CONF_PASSWORD],
)
except AirPatrolAuthenticationError as auth_err:
raise ConfigEntryAuthFailed(
"Authentication with AirPatrol failed"
) from auth_err
self.hass.config_entries.async_update_entry(
self.config_entry,
data={
**self.config_entry.data,
CONF_ACCESS_TOKEN: self.api.get_access_token(),
},
)
async def _setup_client(self) -> None:
"""Set up the AirPatrol API client from stored access_token."""
session = async_get_clientsession(self.hass)
api = AirPatrolAPI(
session,
self.config_entry.data[CONF_ACCESS_TOKEN],
self.config_entry.unique_id,
)
try:
await api.get_data()
except AirPatrolAuthenticationError:
await self._update_token()
self.api = api

View File

@@ -1,44 +0,0 @@
"""Base entity for AirPatrol integration."""
from __future__ import annotations
from typing import Any
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import AirPatrolDataUpdateCoordinator
class AirPatrolEntity(CoordinatorEntity[AirPatrolDataUpdateCoordinator]):
"""Base entity for AirPatrol devices."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: AirPatrolDataUpdateCoordinator,
unit_id: str,
) -> None:
"""Initialize the AirPatrol entity."""
super().__init__(coordinator)
self._unit_id = unit_id
device = coordinator.data[unit_id]
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unit_id)},
name=device["name"],
manufacturer=device["manufacturer"],
model=device["model"],
serial_number=device["hwid"],
)
@property
def device_data(self) -> dict[str, Any]:
"""Return the device data."""
return self.coordinator.data[self._unit_id]
@property
def available(self) -> bool:
"""Return if entity is available."""
return super().available and self._unit_id in self.coordinator.data

View File

@@ -1,11 +0,0 @@
{
"domain": "airpatrol",
"name": "AirPatrol",
"codeowners": ["@antondalgren"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airpatrol",
"integration_type": "device",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["airpatrol==0.1.0"]
}

View File

@@ -1,65 +0,0 @@
rules:
# Bronze
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: Integration does not provide custom actions
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
entity-event-setup:
status: exempt
comment: |
Entities doesn't 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: done
config-entry-unloading: done
docs-configuration-parameters: done
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: todo
discovery-update-info: todo
discovery: todo
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: todo
entity-category: done
entity-device-class: done
entity-disabled-by-default: todo
entity-translations: done
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues: todo
stale-devices: todo
# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo

View File

@@ -1,38 +0,0 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"unique_id_mismatch": "Login credentials do not match the configured account"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "[%key:component::airpatrol::config::step::user::data_description::email%]",
"password": "[%key:component::airpatrol::config::step::user::data_description::password%]"
},
"description": "Reauthenticate with AirPatrol"
},
"user": {
"data": {
"email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"email": "Your AirPatrol email address",
"password": "Your AirPatrol password"
},
"description": "Connect to AirPatrol"
}
}
}
}

View File

@@ -17,7 +17,6 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/airthings", "documentation": "https://www.home-assistant.io/integrations/airthings",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["airthings"], "loggers": ["airthings"],
"requirements": ["airthings-cloud==0.2.0"] "requirements": ["airthings-cloud==0.2.0"]

View File

@@ -27,7 +27,6 @@
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/airthings_ble", "documentation": "https://www.home-assistant.io/integrations/airthings_ble",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["airthings-ble==1.2.0"] "requirements": ["airthings-ble==1.2.0"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@samsinnamon"], "codeowners": ["@samsinnamon"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch4", "documentation": "https://www.home-assistant.io/integrations/airtouch4",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["airtouch4pyapi"], "loggers": ["airtouch4pyapi"],
"requirements": ["airtouch4pyapi==1.0.5"] "requirements": ["airtouch4pyapi==1.0.5"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@danzel"], "codeowners": ["@danzel"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airtouch5", "documentation": "https://www.home-assistant.io/integrations/airtouch5",
"integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["airtouch5py"], "loggers": ["airtouch5py"],
"requirements": ["airtouch5py==0.3.0"] "requirements": ["airtouch5py==0.3.0"]

View File

@@ -9,8 +9,7 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/airzone", "documentation": "https://www.home-assistant.io/integrations/airzone",
"integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioairzone"], "loggers": ["aioairzone"],
"requirements": ["aioairzone==1.0.4"] "requirements": ["aioairzone==1.0.2"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@Noltari"], "codeowners": ["@Noltari"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.7.2"] "requirements": ["aioairzone-cloud==0.7.2"]

View File

@@ -4,10 +4,10 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_supported_features from homeassistant.helpers.entity import get_supported_features
from homeassistant.helpers.trigger import ( from homeassistant.helpers.trigger import (
EntityTargetStateTriggerBase, EntityStateTriggerBase,
Trigger, Trigger,
make_entity_target_state_trigger, make_conditional_entity_state_trigger,
make_entity_transition_trigger, make_entity_state_trigger,
) )
from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState from .const import DOMAIN, AlarmControlPanelEntityFeature, AlarmControlPanelState
@@ -21,7 +21,7 @@ def supports_feature(hass: HomeAssistant, entity_id: str, features: int) -> bool
return False return False
class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase): class EntityStateTriggerRequiredFeatures(EntityStateTriggerBase):
"""Trigger for entity state changes.""" """Trigger for entity state changes."""
_required_features: int _required_features: int
@@ -38,7 +38,7 @@ class EntityStateTriggerRequiredFeatures(EntityTargetStateTriggerBase):
def make_entity_state_trigger_required_features( def make_entity_state_trigger_required_features(
domain: str, to_state: str, required_features: int domain: str, to_state: str, required_features: int
) -> type[EntityTargetStateTriggerBase]: ) -> type[EntityStateTriggerBase]:
"""Create an entity state trigger class.""" """Create an entity state trigger class."""
class CustomTrigger(EntityStateTriggerRequiredFeatures): class CustomTrigger(EntityStateTriggerRequiredFeatures):
@@ -52,7 +52,7 @@ def make_entity_state_trigger_required_features(
TRIGGERS: dict[str, type[Trigger]] = { TRIGGERS: dict[str, type[Trigger]] = {
"armed": make_entity_transition_trigger( "armed": make_conditional_entity_state_trigger(
DOMAIN, DOMAIN,
from_states={ from_states={
AlarmControlPanelState.ARMING, AlarmControlPanelState.ARMING,
@@ -89,12 +89,8 @@ TRIGGERS: dict[str, type[Trigger]] = {
AlarmControlPanelState.ARMED_VACATION, AlarmControlPanelState.ARMED_VACATION,
AlarmControlPanelEntityFeature.ARM_VACATION, AlarmControlPanelEntityFeature.ARM_VACATION,
), ),
"disarmed": make_entity_target_state_trigger( "disarmed": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.DISARMED),
DOMAIN, AlarmControlPanelState.DISARMED "triggered": make_entity_state_trigger(DOMAIN, AlarmControlPanelState.TRIGGERED),
),
"triggered": make_entity_target_state_trigger(
DOMAIN, AlarmControlPanelState.TRIGGERED
),
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@madpilot"], "codeowners": ["@madpilot"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/amberelectric", "documentation": "https://www.home-assistant.io/integrations/amberelectric",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["amberelectric"], "loggers": ["amberelectric"],
"requirements": ["amberelectric==2.0.12"] "requirements": ["amberelectric==2.0.12"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@engrbm87"], "codeowners": ["@engrbm87"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/android_ip_webcam", "documentation": "https://www.home-assistant.io/integrations/android_ip_webcam",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["pydroid-ipcam==3.0.0"] "requirements": ["pydroid-ipcam==3.0.0"]
} }

View File

@@ -39,6 +39,7 @@ async def async_setup_entry(
cookie_jar=CookieJar(quote_cookie=False), cookie_jar=CookieJar(quote_cookie=False),
), ),
refresh_token=entry.data[CONF_ACCESS_TOKEN], refresh_token=entry.data[CONF_ACCESS_TOKEN],
account_number=entry.data[CONF_ACCOUNT_NUMBER],
) )
try: try:
await auth.send_refresh_request() await auth.send_refresh_request()
@@ -48,7 +49,7 @@ async def async_setup_entry(
_aw = AnglianWater(authenticator=auth) _aw = AnglianWater(authenticator=auth)
try: try:
await _aw.validate_smart_meter(entry.data[CONF_ACCOUNT_NUMBER]) await _aw.validate_smart_meter()
except SmartMeterUnavailableError as err: except SmartMeterUnavailableError as err:
raise ConfigEntryError( raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="smart_meter_unavailable" translation_domain=DOMAIN, translation_key="smart_meter_unavailable"

View File

@@ -7,7 +7,7 @@ from typing import Any
from aiohttp import CookieJar from aiohttp import CookieJar
from pyanglianwater import AnglianWater from pyanglianwater import AnglianWater
from pyanglianwater.auth import MSOB2CAuth from pyanglianwater.auth import BaseAuth, MSOB2CAuth
from pyanglianwater.exceptions import ( from pyanglianwater.exceptions import (
InvalidAccountIdError, InvalidAccountIdError,
SelfAssertedError, SelfAssertedError,
@@ -30,14 +30,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Required(CONF_PASSWORD): selector.TextSelector( vol.Required(CONF_PASSWORD): selector.TextSelector(
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD) selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD)
), ),
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
} }
) )
async def validate_credentials( async def validate_credentials(auth: MSOB2CAuth) -> str | MSOB2CAuth:
auth: MSOB2CAuth, account_number: str
) -> str | MSOB2CAuth:
"""Validate the provided credentials.""" """Validate the provided credentials."""
try: try:
await auth.send_login_request() await auth.send_login_request()
@@ -48,7 +45,7 @@ async def validate_credentials(
return "unknown" return "unknown"
_aw = AnglianWater(authenticator=auth) _aw = AnglianWater(authenticator=auth)
try: try:
await _aw.validate_smart_meter(account_number) await _aw.validate_smart_meter()
except (InvalidAccountIdError, SmartMeterUnavailableError): except (InvalidAccountIdError, SmartMeterUnavailableError):
return "smart_meter_unavailable" return "smart_meter_unavailable"
return auth return auth
@@ -71,21 +68,35 @@ class AnglianWaterConfigFlow(ConfigFlow, domain=DOMAIN):
self.hass, self.hass,
cookie_jar=CookieJar(quote_cookie=False), cookie_jar=CookieJar(quote_cookie=False),
), ),
), account_number=user_input.get(CONF_ACCOUNT_NUMBER),
user_input[CONF_ACCOUNT_NUMBER], )
) )
if isinstance(validation_response, str): if isinstance(validation_response, BaseAuth):
errors["base"] = validation_response account_number = (
else: user_input.get(CONF_ACCOUNT_NUMBER)
await self.async_set_unique_id(user_input[CONF_ACCOUNT_NUMBER]) or validation_response.account_number
)
await self.async_set_unique_id(account_number)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=user_input[CONF_ACCOUNT_NUMBER], title=account_number,
data={ data={
**user_input, **user_input,
CONF_ACCESS_TOKEN: validation_response.refresh_token, CONF_ACCESS_TOKEN: validation_response.refresh_token,
CONF_ACCOUNT_NUMBER: account_number,
}, },
) )
if validation_response == "smart_meter_unavailable":
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA.extend(
{
vol.Required(CONF_ACCOUNT_NUMBER): selector.TextSelector(),
}
),
errors={"base": validation_response},
)
errors["base"] = validation_response
return self.async_show_form( return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors

View File

@@ -12,7 +12,7 @@ from homeassistant.config_entries import ConfigEntry
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 CONF_ACCOUNT_NUMBER, DOMAIN from .const import DOMAIN
type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator] type AnglianWaterConfigEntry = ConfigEntry[AnglianWaterUpdateCoordinator]
@@ -44,6 +44,6 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]):
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Update data from Anglian Water's API.""" """Update data from Anglian Water's API."""
try: try:
return await self.api.update(self.config_entry.data[CONF_ACCOUNT_NUMBER]) return await self.api.update()
except (ExpiredAccessTokenError, UnknownEndpointError) as err: except (ExpiredAccessTokenError, UnknownEndpointError) as err:
raise UpdateFailed from err raise UpdateFailed from err

View File

@@ -4,9 +4,7 @@
"codeowners": ["@pantherale0"], "codeowners": ["@pantherale0"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anglian_water", "documentation": "https://www.home-assistant.io/integrations/anglian_water",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["pyanglianwater"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["pyanglianwater==3.1.0"] "requirements": ["pyanglianwater==2.1.0"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@Lash-L"], "codeowners": ["@Lash-L"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anova", "documentation": "https://www.home-assistant.io/integrations/anova",
"integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["anova_wifi"], "loggers": ["anova_wifi"],
"requirements": ["anova-wifi==0.17.0"] "requirements": ["anova-wifi==0.17.0"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@hyralex"], "codeowners": ["@hyralex"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/anthemav", "documentation": "https://www.home-assistant.io/integrations/anthemav",
"integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["anthemav"], "loggers": ["anthemav"],
"requirements": ["anthemav==1.4.1"] "requirements": ["anthemav==1.4.1"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@bdr99"], "codeowners": ["@bdr99"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith", "documentation": "https://www.home-assistant.io/integrations/aosmith",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.15"] "requirements": ["py-aosmith==1.0.15"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@yuxincs"], "codeowners": ["@yuxincs"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/apcupsd", "documentation": "https://www.home-assistant.io/integrations/apcupsd",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["apcaccess"], "loggers": ["apcaccess"],
"quality_scale": "platinum", "quality_scale": "platinum",

View File

@@ -4,7 +4,6 @@
"codeowners": ["@elupus"], "codeowners": ["@elupus"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["arcam"], "loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.2"], "requirements": ["arcam-fmj==1.8.2"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@ikalnyi"], "codeowners": ["@ikalnyi"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/arve", "documentation": "https://www.home-assistant.io/integrations/arve",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["asyncarve==0.1.1"] "requirements": ["asyncarve==0.1.1"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@milanmeu"], "codeowners": ["@milanmeu"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aseko_pool_live", "documentation": "https://www.home-assistant.io/integrations/aseko_pool_live",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioaseko"], "loggers": ["aioaseko"],
"requirements": ["aioaseko==1.0.0"] "requirements": ["aioaseko==1.0.0"]

View File

@@ -3,9 +3,8 @@
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
import math
from pysilero_vad import SileroVoiceActivityDetector from pymicro_vad import MicroVad
from pyspeex_noise import AudioProcessor from pyspeex_noise import AudioProcessor
from .const import BYTES_PER_CHUNK from .const import BYTES_PER_CHUNK
@@ -43,8 +42,8 @@ class AudioEnhancer(ABC):
"""Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples.""" """Enhance chunk of PCM audio @ 16Khz with 16-bit mono samples."""
class SileroVadSpeexEnhancer(AudioEnhancer): class MicroVadSpeexEnhancer(AudioEnhancer):
"""Audio enhancer that runs Silero VAD and speex.""" """Audio enhancer that runs microVAD and speex."""
def __init__( def __init__(
self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool self, auto_gain: int, noise_suppression: int, is_vad_enabled: bool
@@ -70,49 +69,21 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
self.noise_suppression, self.noise_suppression,
) )
self.vad: SileroVoiceActivityDetector | None = None self.vad: MicroVad | None = None
# We get 10ms chunks but Silero works on 32ms chunks, so we have to
# buffer audio. The previous speech probability is used until enough
# audio has been buffered.
self._vad_buffer: bytearray | None = None
self._vad_buffer_chunks = 0
self._vad_buffer_chunk_idx = 0
self._last_speech_probability: float | None = None
if self.is_vad_enabled: if self.is_vad_enabled:
self.vad = SileroVoiceActivityDetector() self.vad = MicroVad()
_LOGGER.debug("Initialized microVAD")
# VAD buffer is a multiple of 10ms, but Silero VAD needs 32ms.
self._vad_buffer_chunks = int(
math.ceil(self.vad.chunk_bytes() / BYTES_PER_CHUNK)
)
self._vad_leftover_bytes = self.vad.chunk_bytes() - BYTES_PER_CHUNK
self._vad_buffer = bytearray(self.vad.chunk_bytes())
_LOGGER.debug("Initialized Silero VAD")
def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk: def enhance_chunk(self, audio: bytes, timestamp_ms: int) -> EnhancedAudioChunk:
"""Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples.""" """Enhance 10ms chunk of PCM audio @ 16Khz with 16-bit mono samples."""
speech_probability: float | None = None
assert len(audio) == BYTES_PER_CHUNK assert len(audio) == BYTES_PER_CHUNK
if self.vad is not None: if self.vad is not None:
# Run VAD # Run VAD
assert self._vad_buffer is not None speech_probability = self.vad.Process10ms(audio)
start_idx = self._vad_buffer_chunk_idx * BYTES_PER_CHUNK
self._vad_buffer[start_idx : start_idx + BYTES_PER_CHUNK] = audio
self._vad_buffer_chunk_idx += 1
if self._vad_buffer_chunk_idx >= self._vad_buffer_chunks:
# We have enough data to run Silero VAD (32 ms)
self._last_speech_probability = self.vad.process_chunk(
self._vad_buffer[: self.vad.chunk_bytes()]
)
# Copy leftover audio that wasn't processed to start
self._vad_buffer[: self._vad_leftover_bytes] = self._vad_buffer[
-self._vad_leftover_bytes :
]
self._vad_buffer_chunk_idx = 0
if self.audio_processor is not None: if self.audio_processor is not None:
# Run noise suppression and auto gain # Run noise suppression and auto gain
@@ -121,5 +92,5 @@ class SileroVadSpeexEnhancer(AudioEnhancer):
return EnhancedAudioChunk( return EnhancedAudioChunk(
audio=audio, audio=audio,
timestamp_ms=timestamp_ms, timestamp_ms=timestamp_ms,
speech_probability=self._last_speech_probability, speech_probability=speech_probability,
) )

View File

@@ -8,5 +8,5 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "local_push", "iot_class": "local_push",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["pysilero-vad==3.0.1", "pyspeex-noise==1.0.2"] "requirements": ["pymicro-vad==1.0.1", "pyspeex-noise==1.0.2"]
} }

View File

@@ -55,7 +55,7 @@ from homeassistant.util import (
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
from homeassistant.util.limited_size_dict import LimitedSizeDict from homeassistant.util.limited_size_dict import LimitedSizeDict
from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, SileroVadSpeexEnhancer from .audio_enhancer import AudioEnhancer, EnhancedAudioChunk, MicroVadSpeexEnhancer
from .const import ( from .const import (
ACKNOWLEDGE_PATH, ACKNOWLEDGE_PATH,
BYTES_PER_CHUNK, BYTES_PER_CHUNK,
@@ -633,7 +633,7 @@ class PipelineRun:
# Initialize with audio settings # Initialize with audio settings
if self.audio_settings.needs_processor and (self.audio_enhancer is None): if self.audio_settings.needs_processor and (self.audio_enhancer is None):
# Default audio enhancer # Default audio enhancer
self.audio_enhancer = SileroVadSpeexEnhancer( self.audio_enhancer = MicroVadSpeexEnhancer(
self.audio_settings.auto_gain_dbfs, self.audio_settings.auto_gain_dbfs,
self.audio_settings.noise_suppression_level, self.audio_settings.noise_suppression_level,
self.audio_settings.is_vad_enabled, self.audio_settings.is_vad_enabled,

View File

@@ -1,22 +1,16 @@
"""Provides triggers for assist satellites.""" """Provides triggers for assist satellites."""
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger from homeassistant.helpers.trigger import Trigger, make_entity_state_trigger
from .const import DOMAIN from .const import DOMAIN
from .entity import AssistSatelliteState from .entity import AssistSatelliteState
TRIGGERS: dict[str, type[Trigger]] = { TRIGGERS: dict[str, type[Trigger]] = {
"idle": make_entity_target_state_trigger(DOMAIN, AssistSatelliteState.IDLE), "idle": make_entity_state_trigger(DOMAIN, AssistSatelliteState.IDLE),
"listening": make_entity_target_state_trigger( "listening": make_entity_state_trigger(DOMAIN, AssistSatelliteState.LISTENING),
DOMAIN, AssistSatelliteState.LISTENING "processing": make_entity_state_trigger(DOMAIN, AssistSatelliteState.PROCESSING),
), "responding": make_entity_state_trigger(DOMAIN, AssistSatelliteState.RESPONDING),
"processing": make_entity_target_state_trigger(
DOMAIN, AssistSatelliteState.PROCESSING
),
"responding": make_entity_target_state_trigger(
DOMAIN, AssistSatelliteState.RESPONDING
),
} }

View File

@@ -7,5 +7,5 @@
"integration_type": "hub", "integration_type": "hub",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["aioasuswrt", "asusrouter", "asyncssh"], "loggers": ["aioasuswrt", "asusrouter", "asyncssh"],
"requirements": ["aioasuswrt==1.5.4", "asusrouter==1.21.3"] "requirements": ["aioasuswrt==1.5.1", "asusrouter==1.21.0"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@MatsNL"], "codeowners": ["@MatsNL"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/atag", "documentation": "https://www.home-assistant.io/integrations/atag",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["pyatag"], "loggers": ["pyatag"],
"requirements": ["pyatag==0.3.5.3"] "requirements": ["pyatag==0.3.5.3"]

View File

@@ -27,8 +27,7 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/august", "documentation": "https://www.home-assistant.io/integrations/august",
"integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"], "loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.2"] "requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.1"]
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@djtimca"], "codeowners": ["@djtimca"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aurora", "documentation": "https://www.home-assistant.io/integrations/aurora",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["auroranoaa"], "loggers": ["auroranoaa"],
"requirements": ["auroranoaa==0.0.5"] "requirements": ["auroranoaa==0.0.5"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@nickw444", "@Bre77"], "codeowners": ["@nickw444", "@Bre77"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aussie_broadband", "documentation": "https://www.home-assistant.io/integrations/aussie_broadband",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aussiebb"], "loggers": ["aussiebb"],
"requirements": ["pyaussiebb==0.1.5"] "requirements": ["pyaussiebb==0.1.5"]

View File

@@ -4,8 +4,6 @@
"codeowners": ["@klaasnicolaas"], "codeowners": ["@klaasnicolaas"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/autarco", "documentation": "https://www.home-assistant.io/integrations/autarco",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["autarco==3.2.0"] "requirements": ["autarco==3.2.0"]
} }

View File

@@ -6,7 +6,10 @@ rules:
This integration does not provide additional actions. This integration does not provide additional actions.
appropriate-polling: done appropriate-polling: done
brands: done brands: done
common-modules: done common-modules:
status: todo
comment: |
The entity.py file is not used in this integration.
config-flow-test-coverage: done config-flow-test-coverage: done
config-flow: done config-flow: done
dependency-transparency: done dependency-transparency: done

View File

@@ -204,25 +204,13 @@ async def async_setup_entry(
async_add_entities(entities) async_add_entities(entities)
class AutarcoSensorBase(CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity): class AutarcoBatterySensorEntity(
"""Base class for Autarco sensors.""" CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
_attr_has_entity_name = True
def __init__(
self,
coordinator: AutarcoDataUpdateCoordinator,
description: SensorEntityDescription,
) -> None:
"""Initialize Autarco sensor base."""
super().__init__(coordinator)
self.entity_description = description
class AutarcoBatterySensorEntity(AutarcoSensorBase):
"""Defines an Autarco battery sensor.""" """Defines an Autarco battery sensor."""
entity_description: AutarcoBatterySensorEntityDescription entity_description: AutarcoBatterySensorEntityDescription
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
@@ -230,8 +218,10 @@ class AutarcoBatterySensorEntity(AutarcoSensorBase):
coordinator: AutarcoDataUpdateCoordinator, coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoBatterySensorEntityDescription, description: AutarcoBatterySensorEntityDescription,
) -> None: ) -> None:
"""Initialize Autarco battery sensor.""" """Initialize Autarco sensor."""
super().__init__(coordinator, description) super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = ( self._attr_unique_id = (
f"{coordinator.account_site.site_id}_battery_{description.key}" f"{coordinator.account_site.site_id}_battery_{description.key}"
) )
@@ -249,10 +239,13 @@ class AutarcoBatterySensorEntity(AutarcoSensorBase):
return self.entity_description.value_fn(self.coordinator.data.battery) return self.entity_description.value_fn(self.coordinator.data.battery)
class AutarcoSolarSensorEntity(AutarcoSensorBase): class AutarcoSolarSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
"""Defines an Autarco solar sensor.""" """Defines an Autarco solar sensor."""
entity_description: AutarcoSolarSensorEntityDescription entity_description: AutarcoSolarSensorEntityDescription
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
@@ -260,8 +253,10 @@ class AutarcoSolarSensorEntity(AutarcoSensorBase):
coordinator: AutarcoDataUpdateCoordinator, coordinator: AutarcoDataUpdateCoordinator,
description: AutarcoSolarSensorEntityDescription, description: AutarcoSolarSensorEntityDescription,
) -> None: ) -> None:
"""Initialize Autarco solar sensor.""" """Initialize Autarco sensor."""
super().__init__(coordinator, description) super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = ( self._attr_unique_id = (
f"{coordinator.account_site.site_id}_solar_{description.key}" f"{coordinator.account_site.site_id}_solar_{description.key}"
) )
@@ -278,10 +273,13 @@ class AutarcoSolarSensorEntity(AutarcoSensorBase):
return self.entity_description.value_fn(self.coordinator.data.solar) return self.entity_description.value_fn(self.coordinator.data.solar)
class AutarcoInverterSensorEntity(AutarcoSensorBase): class AutarcoInverterSensorEntity(
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
):
"""Defines an Autarco inverter sensor.""" """Defines an Autarco inverter sensor."""
entity_description: AutarcoInverterSensorEntityDescription entity_description: AutarcoInverterSensorEntityDescription
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
@@ -290,8 +288,10 @@ class AutarcoInverterSensorEntity(AutarcoSensorBase):
description: AutarcoInverterSensorEntityDescription, description: AutarcoInverterSensorEntityDescription,
serial_number: str, serial_number: str,
) -> None: ) -> None:
"""Initialize Autarco inverter sensor.""" """Initialize Autarco sensor."""
super().__init__(coordinator, description) super().__init__(coordinator)
self.entity_description = description
self._serial_number = serial_number self._serial_number = serial_number
self._attr_unique_id = f"{serial_number}_{description.key}" self._attr_unique_id = f"{serial_number}_{description.key}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(

View File

@@ -125,18 +125,13 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
"alarm_control_panel", "alarm_control_panel",
"assist_satellite", "assist_satellite",
"binary_sensor", "binary_sensor",
"button",
"climate", "climate",
"cover", "cover",
"device_tracker",
"fan", "fan",
"lawn_mower", "lawn_mower",
"light", "light",
"lock",
"media_player", "media_player",
"switch",
"text", "text",
"update",
"vacuum", "vacuum",
} }

View File

@@ -4,7 +4,6 @@
"codeowners": ["@kaareseras"], "codeowners": ["@kaareseras"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_data_explorer", "documentation": "https://www.home-assistant.io/integrations/azure_data_explorer",
"integration_type": "service",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["azure"], "loggers": ["azure"],
"requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"] "requirements": ["azure-kusto-ingest==4.5.1", "azure-kusto-data[aio]==4.5.1"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@timmo001"], "codeowners": ["@timmo001"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_devops", "documentation": "https://www.home-assistant.io/integrations/azure_devops",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioazuredevops"], "loggers": ["aioazuredevops"],
"requirements": ["aioazuredevops==2.2.2"] "requirements": ["aioazuredevops==2.2.2"]

View File

@@ -4,7 +4,6 @@
"codeowners": ["@eavanvalkenburg"], "codeowners": ["@eavanvalkenburg"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/azure_event_hub", "documentation": "https://www.home-assistant.io/integrations/azure_event_hub",
"integration_type": "service",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["azure"], "loggers": ["azure"],
"requirements": ["azure-eventhub==5.11.1"], "requirements": ["azure-eventhub==5.11.1"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@bdraco", "@jfroy"], "codeowners": ["@bdraco", "@jfroy"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/baf", "documentation": "https://www.home-assistant.io/integrations/baf",
"integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["aiobafi6==0.9.0"], "requirements": ["aiobafi6==0.9.0"],
"zeroconf": [ "zeroconf": [

View File

@@ -12,7 +12,6 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/balboa", "documentation": "https://www.home-assistant.io/integrations/balboa",
"integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pybalboa"], "loggers": ["pybalboa"],
"requirements": ["pybalboa==1.1.3"] "requirements": ["pybalboa==1.1.3"]

View File

@@ -21,29 +21,29 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.util.ssl import get_default_context from homeassistant.util.ssl import get_default_context
from .const import DOMAIN from .const import DOMAIN
from .websocket import BeoWebsocket from .websocket import BangOlufsenWebsocket
@dataclass @dataclass
class BeoData: class BangOlufsenData:
"""Dataclass for API client and WebSocket client.""" """Dataclass for API client and WebSocket client."""
websocket: BeoWebsocket websocket: BangOlufsenWebsocket
client: MozartClient client: MozartClient
type BeoConfigEntry = ConfigEntry[BeoData] type BangOlufsenConfigEntry = ConfigEntry[BangOlufsenData]
PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER] PLATFORMS = [Platform.EVENT, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: BangOlufsenConfigEntry) -> bool:
"""Set up from a config entry.""" """Set up from a config entry."""
# Remove casts to str # Remove casts to str
assert entry.unique_id assert entry.unique_id
# Create device now as BeoWebsocket needs a device for debug logging, firing events etc. # Create device now as BangOlufsenWebsocket needs a device for debug logging, firing events etc.
device_registry = dr.async_get(hass) device_registry = dr.async_get(hass)
device_registry.async_get_or_create( device_registry.async_get_or_create(
config_entry_id=entry.entry_id, config_entry_id=entry.entry_id,
@@ -68,10 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
await client.close_api_client() await client.close_api_client()
raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error raise ConfigEntryNotReady(f"Unable to connect to {entry.title}") from error
websocket = BeoWebsocket(hass, entry, client) websocket = BangOlufsenWebsocket(hass, entry, client)
# Add the websocket and API client # Add the websocket and API client
entry.runtime_data = BeoData(websocket, client) entry.runtime_data = BangOlufsenData(websocket, client)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -82,7 +82,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool:
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: BeoConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, entry: BangOlufsenConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
# Close the API client and WebSocket notification listener # Close the API client and WebSocket notification listener
entry.runtime_data.client.disconnect_notifications() entry.runtime_data.client.disconnect_notifications()

View File

@@ -47,7 +47,7 @@ _exception_map = {
} }
class BeoConfigFlowHandler(ConfigFlow, domain=DOMAIN): class BangOlufsenConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Handle a config flow.""" """Handle a config flow."""
_beolink_jid = "" _beolink_jid = ""

View File

@@ -14,7 +14,7 @@ from homeassistant.components.media_player import (
) )
class BeoSource: class BangOlufsenSource:
"""Class used for associating device source ids with friendly names. May not include all sources.""" """Class used for associating device source ids with friendly names. May not include all sources."""
DEEZER: Final[Source] = Source(name="Deezer", id="deezer") DEEZER: Final[Source] = Source(name="Deezer", id="deezer")
@@ -22,18 +22,17 @@ class BeoSource:
NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio") NET_RADIO: Final[Source] = Source(name="B&O Radio", id="netRadio")
SPDIF: Final[Source] = Source(name="Optical", id="spdif") SPDIF: Final[Source] = Source(name="Optical", id="spdif")
TIDAL: Final[Source] = Source(name="Tidal", id="tidal") TIDAL: Final[Source] = Source(name="Tidal", id="tidal")
TV: Final[Source] = Source(name="TV", id="tv")
UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown") UNKNOWN: Final[Source] = Source(name="Unknown Source", id="unknown")
URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer") URI_STREAMER: Final[Source] = Source(name="Audio Streamer", id="uriStreamer")
BEO_STATES: dict[str, MediaPlayerState] = { BANG_OLUFSEN_STATES: dict[str, MediaPlayerState] = {
# Dict used for translating device states to Home Assistant states. # Dict used for translating device states to Home Assistant states.
"started": MediaPlayerState.PLAYING, "started": MediaPlayerState.PLAYING,
"buffering": MediaPlayerState.PLAYING, "buffering": MediaPlayerState.PLAYING,
"idle": MediaPlayerState.IDLE, "idle": MediaPlayerState.IDLE,
"paused": MediaPlayerState.PAUSED, "paused": MediaPlayerState.PAUSED,
"stopped": MediaPlayerState.IDLE, "stopped": MediaPlayerState.PAUSED,
"ended": MediaPlayerState.PAUSED, "ended": MediaPlayerState.PAUSED,
"error": MediaPlayerState.IDLE, "error": MediaPlayerState.IDLE,
# A device's initial state is "unknown" and should be treated as "idle" # A device's initial state is "unknown" and should be treated as "idle"
@@ -41,31 +40,30 @@ BEO_STATES: dict[str, MediaPlayerState] = {
} }
# Dict used for translating Home Assistant settings to device repeat settings. # Dict used for translating Home Assistant settings to device repeat settings.
BEO_REPEAT_FROM_HA: dict[RepeatMode, str] = { BANG_OLUFSEN_REPEAT_FROM_HA: dict[RepeatMode, str] = {
RepeatMode.ALL: "all", RepeatMode.ALL: "all",
RepeatMode.ONE: "track", RepeatMode.ONE: "track",
RepeatMode.OFF: "none", RepeatMode.OFF: "none",
} }
# Dict used for translating device repeat settings to Home Assistant settings. # Dict used for translating device repeat settings to Home Assistant settings.
BEO_REPEAT_TO_HA: dict[str, RepeatMode] = { BANG_OLUFSEN_REPEAT_TO_HA: dict[str, RepeatMode] = {
value: key for key, value in BEO_REPEAT_FROM_HA.items() value: key for key, value in BANG_OLUFSEN_REPEAT_FROM_HA.items()
} }
# Media types for play_media # Media types for play_media
class BeoMediaType(StrEnum): class BangOlufsenMediaType(StrEnum):
"""Bang & Olufsen specific media types.""" """Bang & Olufsen specific media types."""
DEEZER = "deezer"
FAVOURITE = "favourite" FAVOURITE = "favourite"
OVERLAY_TTS = "overlay_tts" DEEZER = "deezer"
RADIO = "radio" RADIO = "radio"
TIDAL = "tidal" TIDAL = "tidal"
TTS = "provider" TTS = "provider"
TV = "tv" OVERLAY_TTS = "overlay_tts"
class BeoModel(StrEnum): class BangOlufsenModel(StrEnum):
"""Enum for compatible model names.""" """Enum for compatible model names."""
# Mozart devices # Mozart devices
@@ -84,7 +82,7 @@ class BeoModel(StrEnum):
BEOREMOTE_ONE = "Beoremote One" BEOREMOTE_ONE = "Beoremote One"
class BeoAttribute(StrEnum): class BangOlufsenAttribute(StrEnum):
"""Enum for extra_state_attribute keys.""" """Enum for extra_state_attribute keys."""
BEOLINK = "beolink" BEOLINK = "beolink"
@@ -95,7 +93,7 @@ class BeoAttribute(StrEnum):
# Physical "buttons" on devices # Physical "buttons" on devices
class BeoButtons(StrEnum): class BangOlufsenButtons(StrEnum):
"""Enum for device buttons.""" """Enum for device buttons."""
BLUETOOTH = "Bluetooth" BLUETOOTH = "Bluetooth"
@@ -142,7 +140,7 @@ class WebsocketNotification(StrEnum):
DOMAIN: Final[str] = "bang_olufsen" DOMAIN: Final[str] = "bang_olufsen"
# Default values for configuration. # Default values for configuration.
DEFAULT_MODEL: Final[str] = BeoModel.BEOSOUND_BALANCE DEFAULT_MODEL: Final[str] = BangOlufsenModel.BEOSOUND_BALANCE
# Configuration. # Configuration.
CONF_SERIAL_NUMBER: Final = "serial_number" CONF_SERIAL_NUMBER: Final = "serial_number"
@@ -150,7 +148,7 @@ CONF_BEOLINK_JID: Final = "jid"
# Models to choose from in manual configuration. # Models to choose from in manual configuration.
SELECTABLE_MODELS: list[str] = [ SELECTABLE_MODELS: list[str] = [
model.value for model in BeoModel if model != BeoModel.BEOREMOTE_ONE model.value for model in BangOlufsenModel if model != BangOlufsenModel.BEOREMOTE_ONE
] ]
MANUFACTURER: Final[str] = "Bang & Olufsen" MANUFACTURER: Final[str] = "Bang & Olufsen"
@@ -162,15 +160,15 @@ ATTR_ITEM_NUMBER: Final[str] = "in"
ATTR_FRIENDLY_NAME: Final[str] = "fn" ATTR_FRIENDLY_NAME: Final[str] = "fn"
# Power states. # Power states.
BEO_ON: Final[str] = "on" BANG_OLUFSEN_ON: Final[str] = "on"
VALID_MEDIA_TYPES: Final[tuple] = ( VALID_MEDIA_TYPES: Final[tuple] = (
BeoMediaType.FAVOURITE, BangOlufsenMediaType.FAVOURITE,
BeoMediaType.DEEZER, BangOlufsenMediaType.DEEZER,
BeoMediaType.RADIO, BangOlufsenMediaType.RADIO,
BeoMediaType.TTS, BangOlufsenMediaType.TTS,
BeoMediaType.TIDAL, BangOlufsenMediaType.TIDAL,
BeoMediaType.OVERLAY_TTS, BangOlufsenMediaType.OVERLAY_TTS,
MediaType.MUSIC, MediaType.MUSIC,
MediaType.URL, MediaType.URL,
MediaType.CHANNEL, MediaType.CHANNEL,
@@ -248,7 +246,7 @@ FALLBACK_SOURCES: Final[SourceArray] = SourceArray(
) )
# Device events # Device events
BEO_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event" BANG_OLUFSEN_WEBSOCKET_EVENT: Final[str] = f"{DOMAIN}_websocket_event"
# Dict used to translate native Bang & Olufsen event names to string.json compatible ones # Dict used to translate native Bang & Olufsen event names to string.json compatible ones
EVENT_TRANSLATION_MAP: dict[str, str] = { EVENT_TRANSLATION_MAP: dict[str, str] = {
@@ -265,7 +263,7 @@ EVENT_TRANSLATION_MAP: dict[str, str] = {
CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS" CONNECTION_STATUS: Final[str] = "CONNECTION_STATUS"
DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BeoButtons] DEVICE_BUTTONS: Final[list[str]] = [x.value for x in BangOlufsenButtons]
DEVICE_BUTTON_EVENTS: Final[list[str]] = [ DEVICE_BUTTON_EVENTS: Final[list[str]] = [

View File

@@ -10,13 +10,13 @@ from homeassistant.const import CONF_MODEL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from . import BeoConfigEntry from . import BangOlufsenConfigEntry
from .const import DOMAIN from .const import DOMAIN
from .util import get_device_buttons from .util import get_device_buttons
async def async_get_config_entry_diagnostics( async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: BeoConfigEntry hass: HomeAssistant, config_entry: BangOlufsenConfigEntry
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return diagnostics for a config entry.""" """Return diagnostics for a config entry."""

View File

@@ -24,8 +24,8 @@ from homeassistant.helpers.entity import Entity
from .const import DOMAIN from .const import DOMAIN
class BeoBase: class BangOlufsenBase:
"""Base class for Bang & Olufsen Home Assistant objects.""" """Base class for BangOlufsen Home Assistant objects."""
def __init__(self, entry: ConfigEntry, client: MozartClient) -> None: def __init__(self, entry: ConfigEntry, client: MozartClient) -> None:
"""Initialize the object.""" """Initialize the object."""
@@ -51,8 +51,8 @@ class BeoBase:
) )
class BeoEntity(Entity, BeoBase): class BangOlufsenEntity(Entity, BangOlufsenBase):
"""Base Entity for Bang & Olufsen entities.""" """Base Entity for BangOlufsen entities."""
_attr_has_entity_name = True _attr_has_entity_name = True
_attr_should_poll = False _attr_should_poll = False

View File

@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BeoConfigEntry from . import BangOlufsenConfigEntry
from .const import ( from .const import (
BEO_REMOTE_CONTROL_KEYS, BEO_REMOTE_CONTROL_KEYS,
BEO_REMOTE_KEY_EVENTS, BEO_REMOTE_KEY_EVENTS,
@@ -25,10 +25,10 @@ from .const import (
DEVICE_BUTTON_EVENTS, DEVICE_BUTTON_EVENTS,
DOMAIN, DOMAIN,
MANUFACTURER, MANUFACTURER,
BeoModel, BangOlufsenModel,
WebsocketNotification, WebsocketNotification,
) )
from .entity import BeoEntity from .entity import BangOlufsenEntity
from .util import get_device_buttons, get_remotes from .util import get_device_buttons, get_remotes
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -36,14 +36,14 @@ PARALLEL_UPDATES = 0
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: BeoConfigEntry, config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up Event entities from config entry.""" """Set up Event entities from config entry."""
entities: list[BeoEvent] = [] entities: list[BangOlufsenEvent] = []
async_add_entities( async_add_entities(
BeoButtonEvent(config_entry, button_type) BangOlufsenButtonEvent(config_entry, button_type)
for button_type in get_device_buttons(config_entry.data[CONF_MODEL]) for button_type in get_device_buttons(config_entry.data[CONF_MODEL])
) )
@@ -54,7 +54,7 @@ async def async_setup_entry(
# Add Light keys # Add Light keys
entities.extend( entities.extend(
[ [
BeoRemoteKeyEvent( BangOlufsenRemoteKeyEvent(
config_entry, config_entry,
remote, remote,
f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}", f"{BEO_REMOTE_SUBMENU_LIGHT}/{key_type}",
@@ -66,7 +66,7 @@ async def async_setup_entry(
# Add Control keys # Add Control keys
entities.extend( entities.extend(
[ [
BeoRemoteKeyEvent( BangOlufsenRemoteKeyEvent(
config_entry, config_entry,
remote, remote,
f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}", f"{BEO_REMOTE_SUBMENU_CONTROL}/{key_type}",
@@ -84,9 +84,10 @@ async def async_setup_entry(
config_entry.entry_id config_entry.entry_id
) )
for device in devices: for device in devices:
if device.model == BeoModel.BEOREMOTE_ONE and device.serial_number not in { if (
remote.serial_number for remote in remotes device.model == BangOlufsenModel.BEOREMOTE_ONE
}: and device.serial_number not in {remote.serial_number for remote in remotes}
):
device_registry.async_update_device( device_registry.async_update_device(
device.id, remove_config_entry_id=config_entry.entry_id device.id, remove_config_entry_id=config_entry.entry_id
) )
@@ -94,13 +95,13 @@ async def async_setup_entry(
async_add_entities(new_entities=entities) async_add_entities(new_entities=entities)
class BeoEvent(BeoEntity, EventEntity): class BangOlufsenEvent(BangOlufsenEntity, EventEntity):
"""Base Event class.""" """Base Event class."""
_attr_device_class = EventDeviceClass.BUTTON _attr_device_class = EventDeviceClass.BUTTON
_attr_entity_registry_enabled_default = False _attr_entity_registry_enabled_default = False
def __init__(self, config_entry: BeoConfigEntry) -> None: def __init__(self, config_entry: BangOlufsenConfigEntry) -> None:
"""Initialize Event.""" """Initialize Event."""
super().__init__(config_entry, config_entry.runtime_data.client) super().__init__(config_entry, config_entry.runtime_data.client)
@@ -111,12 +112,12 @@ class BeoEvent(BeoEntity, EventEntity):
self.async_write_ha_state() self.async_write_ha_state()
class BeoButtonEvent(BeoEvent): class BangOlufsenButtonEvent(BangOlufsenEvent):
"""Event class for Button events.""" """Event class for Button events."""
_attr_event_types = DEVICE_BUTTON_EVENTS _attr_event_types = DEVICE_BUTTON_EVENTS
def __init__(self, config_entry: BeoConfigEntry, button_type: str) -> None: def __init__(self, config_entry: BangOlufsenConfigEntry, button_type: str) -> None:
"""Initialize Button.""" """Initialize Button."""
super().__init__(config_entry) super().__init__(config_entry)
@@ -145,14 +146,14 @@ class BeoButtonEvent(BeoEvent):
) )
class BeoRemoteKeyEvent(BeoEvent): class BangOlufsenRemoteKeyEvent(BangOlufsenEvent):
"""Event class for Beoremote One key events.""" """Event class for Beoremote One key events."""
_attr_event_types = BEO_REMOTE_KEY_EVENTS _attr_event_types = BEO_REMOTE_KEY_EVENTS
def __init__( def __init__(
self, self,
config_entry: BeoConfigEntry, config_entry: BangOlufsenConfigEntry,
remote: PairedRemote, remote: PairedRemote,
key_type: str, key_type: str,
) -> None: ) -> None:
@@ -165,8 +166,8 @@ class BeoRemoteKeyEvent(BeoEvent):
self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}" self._attr_unique_id = f"{remote.serial_number}_{self._unique_id}_{key_type}"
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")}, identifiers={(DOMAIN, f"{remote.serial_number}_{self._unique_id}")},
name=f"{BeoModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}", name=f"{BangOlufsenModel.BEOREMOTE_ONE}-{remote.serial_number}-{self._unique_id}",
model=BeoModel.BEOREMOTE_ONE, model=BangOlufsenModel.BEOREMOTE_ONE,
serial_number=remote.serial_number, serial_number=remote.serial_number,
sw_version=remote.app_version, sw_version=remote.app_version,
manufacturer=MANUFACTURER, manufacturer=MANUFACTURER,

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==5.3.1.108.0"], "requirements": ["mozart-api==5.1.0.247.1"],
"zeroconf": ["_bangolufsen._tcp.local."] "zeroconf": ["_bangolufsen._tcp.local."]
} }

View File

@@ -69,11 +69,11 @@ from homeassistant.helpers.entity_platform import (
) )
from homeassistant.util.dt import utcnow from homeassistant.util.dt import utcnow
from . import BeoConfigEntry from . import BangOlufsenConfigEntry
from .const import ( from .const import (
BEO_REPEAT_FROM_HA, BANG_OLUFSEN_REPEAT_FROM_HA,
BEO_REPEAT_TO_HA, BANG_OLUFSEN_REPEAT_TO_HA,
BEO_STATES, BANG_OLUFSEN_STATES,
BEOLINK_JOIN_SOURCES, BEOLINK_JOIN_SOURCES,
BEOLINK_JOIN_SOURCES_TO_UPPER, BEOLINK_JOIN_SOURCES_TO_UPPER,
CONF_BEOLINK_JID, CONF_BEOLINK_JID,
@@ -82,12 +82,12 @@ from .const import (
FALLBACK_SOURCES, FALLBACK_SOURCES,
MANUFACTURER, MANUFACTURER,
VALID_MEDIA_TYPES, VALID_MEDIA_TYPES,
BeoAttribute, BangOlufsenAttribute,
BeoMediaType, BangOlufsenMediaType,
BeoSource, BangOlufsenSource,
WebsocketNotification, WebsocketNotification,
) )
from .entity import BeoEntity from .entity import BangOlufsenEntity
from .util import get_serial_number_from_jid from .util import get_serial_number_from_jid
PARALLEL_UPDATES = 0 PARALLEL_UPDATES = 0
@@ -96,7 +96,7 @@ SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
BEO_FEATURES = ( BANG_OLUFSEN_FEATURES = (
MediaPlayerEntityFeature.BROWSE_MEDIA MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.CLEAR_PLAYLIST | MediaPlayerEntityFeature.CLEAR_PLAYLIST
| MediaPlayerEntityFeature.GROUPING | MediaPlayerEntityFeature.GROUPING
@@ -119,13 +119,15 @@ BEO_FEATURES = (
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: BeoConfigEntry, config_entry: BangOlufsenConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up a Media Player entity from config entry.""" """Set up a Media Player entity from config entry."""
# Add MediaPlayer entity # Add MediaPlayer entity
async_add_entities( async_add_entities(
new_entities=[BeoMediaPlayer(config_entry, config_entry.runtime_data.client)], new_entities=[
BangOlufsenMediaPlayer(config_entry, config_entry.runtime_data.client)
],
update_before_add=True, update_before_add=True,
) )
@@ -185,7 +187,7 @@ async def async_setup_entry(
) )
class BeoMediaPlayer(BeoEntity, MediaPlayerEntity): class BangOlufsenMediaPlayer(BangOlufsenEntity, MediaPlayerEntity):
"""Representation of a media player.""" """Representation of a media player."""
_attr_name = None _attr_name = None
@@ -218,7 +220,6 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
self._sources: dict[str, str] = {} self._sources: dict[str, str] = {}
self._state: str = MediaPlayerState.IDLE self._state: str = MediaPlayerState.IDLE
self._video_sources: dict[str, str] = {} self._video_sources: dict[str, str] = {}
self._video_source_id_map: dict[str, str] = {}
self._sound_modes: dict[str, int] = {} self._sound_modes: dict[str, int] = {}
# Beolink compatible sources # Beolink compatible sources
@@ -287,7 +288,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
queue_settings = await self._client.get_settings_queue(_request_timeout=5) queue_settings = await self._client.get_settings_queue(_request_timeout=5)
if queue_settings.repeat is not None: if queue_settings.repeat is not None:
self._attr_repeat = BEO_REPEAT_TO_HA[queue_settings.repeat] self._attr_repeat = BANG_OLUFSEN_REPEAT_TO_HA[queue_settings.repeat]
if queue_settings.shuffle is not None: if queue_settings.shuffle is not None:
self._attr_shuffle = queue_settings.shuffle self._attr_shuffle = queue_settings.shuffle
@@ -356,9 +357,6 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
and menu_item.label != "TV" and menu_item.label != "TV"
): ):
self._video_sources[key] = menu_item.label self._video_sources[key] = menu_item.label
self._video_source_id_map[
menu_item.content.content_uri.removeprefix("tv://")
] = menu_item.label
# Combine the source dicts # Combine the source dicts
self._sources = self._audio_sources | self._video_sources self._sources = self._audio_sources | self._video_sources
@@ -410,8 +408,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Check if source is line-in or optical and progress should be updated # Check if source is line-in or optical and progress should be updated
if self._source_change.id in ( if self._source_change.id in (
BeoSource.LINE_IN.id, BangOlufsenSource.LINE_IN.id,
BeoSource.SPDIF.id, BangOlufsenSource.SPDIF.id,
): ):
self._playback_progress = PlaybackProgress(progress=0) self._playback_progress = PlaybackProgress(progress=0)
@@ -452,8 +450,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add Beolink self # Add Beolink self
self._beolink_attributes = { self._beolink_attributes = {
BeoAttribute.BEOLINK: { BangOlufsenAttribute.BEOLINK: {
BeoAttribute.BEOLINK_SELF: {self.device_entry.name: self._beolink_jid} BangOlufsenAttribute.BEOLINK_SELF: {
self.device_entry.name: self._beolink_jid
}
} }
} }
@@ -461,12 +461,12 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
peers = await self._client.get_beolink_peers() peers = await self._client.get_beolink_peers()
if len(peers) > 0: if len(peers) > 0:
self._beolink_attributes[BeoAttribute.BEOLINK][ self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS BangOlufsenAttribute.BEOLINK_PEERS
] = {} ] = {}
for peer in peers: for peer in peers:
self._beolink_attributes[BeoAttribute.BEOLINK][ self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BeoAttribute.BEOLINK_PEERS BangOlufsenAttribute.BEOLINK_PEERS
][peer.friendly_name] = peer.jid ][peer.friendly_name] = peer.jid
# Add Beolink listeners / leader # Add Beolink listeners / leader
@@ -488,8 +488,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# Add self # Add self
group_members.append(self.entity_id) group_members.append(self.entity_id)
self._beolink_attributes[BeoAttribute.BEOLINK][ self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BeoAttribute.BEOLINK_LEADER BangOlufsenAttribute.BEOLINK_LEADER
] = { ] = {
self._remote_leader.friendly_name: self._remote_leader.jid, self._remote_leader.friendly_name: self._remote_leader.jid,
} }
@@ -527,8 +527,8 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
beolink_listener.jid beolink_listener.jid
) )
break break
self._beolink_attributes[BeoAttribute.BEOLINK][ self._beolink_attributes[BangOlufsenAttribute.BEOLINK][
BeoAttribute.BEOLINK_LISTENERS BangOlufsenAttribute.BEOLINK_LISTENERS
] = beolink_listeners_attribute ] = beolink_listeners_attribute
self._attr_group_members = group_members self._attr_group_members = group_members
@@ -587,7 +587,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
for sound_mode in sound_modes: for sound_mode in sound_modes:
label = f"{sound_mode.name} ({sound_mode.id})" label = f"{sound_mode.name} ({sound_mode.id})"
self._sound_modes[label] = cast(int, sound_mode.id) self._sound_modes[label] = sound_mode.id
if sound_mode.id == active_sound_mode.id: if sound_mode.id == active_sound_mode.id:
self._attr_sound_mode = label self._attr_sound_mode = label
@@ -600,7 +600,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property @property
def supported_features(self) -> MediaPlayerEntityFeature: def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported.""" """Flag media player features that are supported."""
features = BEO_FEATURES features = BANG_OLUFSEN_FEATURES
# Add seeking if supported by the current source # Add seeking if supported by the current source
if self._source_change.is_seekable is True: if self._source_change.is_seekable is True:
@@ -611,7 +611,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property @property
def state(self) -> MediaPlayerState: def state(self) -> MediaPlayerState:
"""Return the current state of the media player.""" """Return the current state of the media player."""
return BEO_STATES[self._state] return BANG_OLUFSEN_STATES[self._state]
@property @property
def volume_level(self) -> float | None: def volume_level(self) -> float | None:
@@ -631,11 +631,10 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
def media_content_type(self) -> MediaType | str | None: def media_content_type(self) -> MediaType | str | None:
"""Return the current media type.""" """Return the current media type."""
content_type = { content_type = {
BeoSource.DEEZER.id: BeoMediaType.DEEZER, BangOlufsenSource.URI_STREAMER.id: MediaType.URL,
BeoSource.NET_RADIO.id: BeoMediaType.RADIO, BangOlufsenSource.DEEZER.id: BangOlufsenMediaType.DEEZER,
BeoSource.TIDAL.id: BeoMediaType.TIDAL, BangOlufsenSource.TIDAL.id: BangOlufsenMediaType.TIDAL,
BeoSource.TV.id: BeoMediaType.TV, BangOlufsenSource.NET_RADIO.id: BangOlufsenMediaType.RADIO,
BeoSource.URI_STREAMER.id: MediaType.URL,
} }
# Hard to determine content type. # Hard to determine content type.
if self._source_change.id in content_type: if self._source_change.id in content_type:
@@ -695,11 +694,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
@property @property
def source(self) -> str | None: def source(self) -> str | None:
"""Return the current audio/video source.""" """Return the current audio source."""
# Associate TV content ID with a video source
if self.media_content_id in self._video_source_id_map:
return self._video_source_id_map[self.media_content_id]
return self._source_change.name return self._source_change.name
@property @property
@@ -770,7 +765,9 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
async def async_set_repeat(self, repeat: RepeatMode) -> None: async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set playback queues to repeat.""" """Set playback queues to repeat."""
await self._client.set_settings_queue( await self._client.set_settings_queue(
play_queue_settings=PlayQueueSettings(repeat=BEO_REPEAT_FROM_HA[repeat]) play_queue_settings=PlayQueueSettings(
repeat=BANG_OLUFSEN_REPEAT_FROM_HA[repeat]
)
) )
async def async_set_shuffle(self, shuffle: bool) -> None: async def async_set_shuffle(self, shuffle: bool) -> None:
@@ -874,7 +871,7 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
self._volume.level.level + offset_volume, 100 self._volume.level.level + offset_volume, 100
) )
if media_type == BeoMediaType.OVERLAY_TTS: if media_type == BangOlufsenMediaType.OVERLAY_TTS:
# Bang & Olufsen cloud TTS # Bang & Olufsen cloud TTS
overlay_play_request.text_to_speech = ( overlay_play_request.text_to_speech = (
OverlayPlayRequestTextToSpeechTextToSpeech( OverlayPlayRequestTextToSpeechTextToSpeech(
@@ -891,14 +888,14 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
# The "provider" media_type may not be suitable for overlay all the time. # The "provider" media_type may not be suitable for overlay all the time.
# Use it for now. # Use it for now.
elif media_type == BeoMediaType.TTS: elif media_type == BangOlufsenMediaType.TTS:
await self._client.post_overlay_play( await self._client.post_overlay_play(
overlay_play_request=OverlayPlayRequest( overlay_play_request=OverlayPlayRequest(
uri=Uri(location=media_id), uri=Uri(location=media_id),
) )
) )
elif media_type == BeoMediaType.RADIO: elif media_type == BangOlufsenMediaType.RADIO:
await self._client.run_provided_scene( await self._client.run_provided_scene(
scene_properties=SceneProperties( scene_properties=SceneProperties(
action_list=[ action_list=[
@@ -910,13 +907,13 @@ class BeoMediaPlayer(BeoEntity, MediaPlayerEntity):
) )
) )
elif media_type == BeoMediaType.FAVOURITE: elif media_type == BangOlufsenMediaType.FAVOURITE:
await self._client.activate_preset(id=int(media_id)) await self._client.activate_preset(id=int(media_id))
elif media_type in (BeoMediaType.DEEZER, BeoMediaType.TIDAL): elif media_type in (BangOlufsenMediaType.DEEZER, BangOlufsenMediaType.TIDAL):
try: try:
# Play Deezer flow. # Play Deezer flow.
if media_id == "flow" and media_type == BeoMediaType.DEEZER: if media_id == "flow" and media_type == BangOlufsenMediaType.DEEZER:
deezer_id = None deezer_id = None
if "id" in kwargs[ATTR_MEDIA_EXTRA]: if "id" in kwargs[ATTR_MEDIA_EXTRA]:

View File

@@ -11,7 +11,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.device_registry import DeviceEntry
from .const import DEVICE_BUTTONS, DOMAIN, BeoButtons, BeoModel from .const import DEVICE_BUTTONS, DOMAIN, BangOlufsenButtons, BangOlufsenModel
def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry: def get_device(hass: HomeAssistant, unique_id: str) -> DeviceEntry:
@@ -40,27 +40,16 @@ async def get_remotes(client: MozartClient) -> list[PairedRemote]:
] ]
def get_device_buttons(model: BeoModel) -> list[str]: def get_device_buttons(model: BangOlufsenModel) -> list[str]:
"""Get supported buttons for a given model.""" """Get supported buttons for a given model."""
# Beoconnect Core does not have any buttons
if model == BeoModel.BEOCONNECT_CORE:
return []
buttons = DEVICE_BUTTONS.copy() buttons = DEVICE_BUTTONS.copy()
# Models that don't have a microphone button # Beosound Premiere does not have a bluetooth button
if model in ( if model == BangOlufsenModel.BEOSOUND_PREMIERE:
BeoModel.BEOSOUND_A5, buttons.remove(BangOlufsenButtons.BLUETOOTH)
BeoModel.BEOSOUND_A9,
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.MICROPHONE)
# Models that don't have a Bluetooth button # Beoconnect Core does not have any buttons
if model in ( elif model == BangOlufsenModel.BEOCONNECT_CORE:
BeoModel.BEOSOUND_A9, buttons = []
BeoModel.BEOSOUND_PREMIERE,
):
buttons.remove(BeoButtons.BLUETOOTH)
return buttons return buttons

View File

@@ -27,20 +27,20 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.enum import try_parse_enum from homeassistant.util.enum import try_parse_enum
from .const import ( from .const import (
BEO_WEBSOCKET_EVENT, BANG_OLUFSEN_WEBSOCKET_EVENT,
CONNECTION_STATUS, CONNECTION_STATUS,
DOMAIN, DOMAIN,
EVENT_TRANSLATION_MAP, EVENT_TRANSLATION_MAP,
BeoModel, BangOlufsenModel,
WebsocketNotification, WebsocketNotification,
) )
from .entity import BeoBase from .entity import BangOlufsenBase
from .util import get_device, get_remotes from .util import get_device, get_remotes
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class BeoWebsocket(BeoBase): class BangOlufsenWebsocket(BangOlufsenBase):
"""The WebSocket listeners.""" """The WebSocket listeners."""
def __init__( def __init__(
@@ -48,7 +48,7 @@ class BeoWebsocket(BeoBase):
) -> None: ) -> None:
"""Initialize the WebSocket listeners.""" """Initialize the WebSocket listeners."""
BeoBase.__init__(self, entry, client) BangOlufsenBase.__init__(self, entry, client)
self.hass = hass self.hass = hass
self._device = get_device(hass, self._unique_id) self._device = get_device(hass, self._unique_id)
@@ -178,7 +178,7 @@ class BeoWebsocket(BeoBase):
self.entry.entry_id self.entry.entry_id
) )
if device.serial_number is not None if device.serial_number is not None
and device.model == BeoModel.BEOREMOTE_ONE and device.model == BangOlufsenModel.BEOREMOTE_ONE
] ]
# Get paired remotes from device # Get paired remotes from device
remote_serial_numbers = [ remote_serial_numbers = [
@@ -274,4 +274,4 @@ class BeoWebsocket(BeoBase):
} }
_LOGGER.debug("%s", debug_notification) _LOGGER.debug("%s", debug_notification)
self.hass.bus.async_fire(BEO_WEBSOCKET_EVENT, debug_notification) self.hass.bus.async_fire(BANG_OLUFSEN_WEBSOCKET_EVENT, debug_notification)

View File

@@ -4,7 +4,7 @@ from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity import get_device_class from homeassistant.helpers.entity import get_device_class
from homeassistant.helpers.trigger import EntityTargetStateTriggerBase, Trigger from homeassistant.helpers.trigger import EntityStateTriggerBase, Trigger
from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from . import DOMAIN, BinarySensorDeviceClass from . import DOMAIN, BinarySensorDeviceClass
@@ -20,7 +20,7 @@ def get_device_class_or_undefined(
return UNDEFINED return UNDEFINED
class BinarySensorOnOffTrigger(EntityTargetStateTriggerBase): class BinarySensorOnOffTrigger(EntityStateTriggerBase):
"""Class for binary sensor on/off triggers.""" """Class for binary sensor on/off triggers."""
_device_class: BinarySensorDeviceClass | None _device_class: BinarySensorDeviceClass | None

View File

@@ -4,7 +4,6 @@
"codeowners": ["@bbx-a", "@swistakm"], "codeowners": ["@bbx-a", "@swistakm"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blebox", "documentation": "https://www.home-assistant.io/integrations/blebox",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["blebox_uniapi"], "loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.0"], "requirements": ["blebox-uniapi==2.5.0"],

View File

@@ -64,12 +64,6 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
if entry.version == 2: if entry.version == 2:
await _reauth_flow_wrapper(hass, entry, data) await _reauth_flow_wrapper(hass, entry, data)
return False return False
if entry.version == 3:
# Migrate device_id to hardware_id for blinkpy 0.25.x OAuth2 compatibility
if "device_id" in data:
data["hardware_id"] = data.pop("device_id")
hass.config_entries.async_update_entry(entry, data=data, version=4)
return True
return True return True

View File

@@ -21,7 +21,7 @@ from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, HARDWARE_ID from .const import DEVICE_ID, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ async def _send_blink_2fa_pin(blink: Blink, pin: str | None) -> bool:
class BlinkConfigFlow(ConfigFlow, domain=DOMAIN): class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a Blink config flow.""" """Handle a Blink config flow."""
VERSION = 4 VERSION = 3
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the blink flow.""" """Initialize the blink flow."""
@@ -53,7 +53,7 @@ class BlinkConfigFlow(ConfigFlow, domain=DOMAIN):
async def _handle_user_input(self, user_input: dict[str, Any]): async def _handle_user_input(self, user_input: dict[str, Any]):
"""Handle user input.""" """Handle user input."""
self.auth = Auth( self.auth = Auth(
{**user_input, "hardware_id": HARDWARE_ID}, {**user_input, "device_id": DEVICE_ID},
no_prompt=True, no_prompt=True,
session=async_get_clientsession(self.hass), session=async_get_clientsession(self.hass),
) )

View File

@@ -3,7 +3,7 @@
from homeassistant.const import Platform from homeassistant.const import Platform
DOMAIN = "blink" DOMAIN = "blink"
HARDWARE_ID = "Home Assistant" DEVICE_ID = "Home Assistant"
CONF_MIGRATE = "migrate" CONF_MIGRATE = "migrate"
CONF_CAMERA = "camera" CONF_CAMERA = "camera"

View File

@@ -1,7 +1,7 @@
{ {
"domain": "blink", "domain": "blink",
"name": "Blink", "name": "Blink",
"codeowners": ["@fronzbot"], "codeowners": ["@fronzbot", "@mkmer"],
"config_flow": true, "config_flow": true,
"dhcp": [ "dhcp": [
{ {
@@ -18,8 +18,7 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/blink", "documentation": "https://www.home-assistant.io/integrations/blink",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["blinkpy"], "loggers": ["blinkpy"],
"requirements": ["blinkpy==0.25.2"] "requirements": ["blinkpy==0.24.1"]
} }

View File

@@ -13,25 +13,32 @@ from bluecurrent_api.exceptions import (
RequestLimitReached, RequestLimitReached,
WebsocketError, WebsocketError,
) )
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_API_TOKEN, Platform from homeassistant.const import CONF_API_TOKEN, CONF_DEVICE_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.exceptions import (
from homeassistant.helpers import config_validation as cv ConfigEntryAuthFailed,
ConfigEntryNotReady,
ServiceValidationError,
)
from homeassistant.helpers import config_validation as cv, device_registry as dr
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import ( from .const import (
BCU_APP,
CHARGEPOINT_SETTINGS, CHARGEPOINT_SETTINGS,
CHARGEPOINT_STATUS, CHARGEPOINT_STATUS,
CHARGING_CARD_ID,
DOMAIN, DOMAIN,
EVSE_ID, EVSE_ID,
LOGGER, LOGGER,
PLUG_AND_CHARGE, PLUG_AND_CHARGE,
SERVICE_START_CHARGE_SESSION,
VALUE, VALUE,
) )
from .services import async_setup_services
type BlueCurrentConfigEntry = ConfigEntry[Connector] type BlueCurrentConfigEntry = ConfigEntry[Connector]
@@ -47,12 +54,13 @@ VALUE_TYPES = [CHARGEPOINT_STATUS, CHARGEPOINT_SETTINGS]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: {
"""Set up Blue Current.""" vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
async_setup_services(hass) vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
return True }
)
async def async_setup_entry( async def async_setup_entry(
@@ -80,6 +88,66 @@ async def async_setup_entry(
return True return True
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up Blue Current."""
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1]
for identifier in device.identifiers
if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)
return True
async def async_unload_entry( async def async_unload_entry(
hass: HomeAssistant, config_entry: BlueCurrentConfigEntry hass: HomeAssistant, config_entry: BlueCurrentConfigEntry
) -> bool: ) -> bool:

View File

@@ -4,7 +4,6 @@
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"], "codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/blue_current", "documentation": "https://www.home-assistant.io/integrations/blue_current",
"integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["bluecurrent_api"], "loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.3.2"] "requirements": ["bluecurrent-api==1.3.2"]

View File

@@ -1,79 +0,0 @@
"""The Blue Current integration."""
from __future__ import annotations
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_DEVICE_ID
from homeassistant.core import HomeAssistant, ServiceCall, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv, device_registry as dr
from .const import BCU_APP, CHARGING_CARD_ID, DOMAIN, SERVICE_START_CHARGE_SESSION
SERVICE_START_CHARGE_SESSION_SCHEMA = vol.Schema(
{
vol.Required(CONF_DEVICE_ID): cv.string,
# When no charging card is provided, use no charging card (BCU_APP = no charging card).
vol.Optional(CHARGING_CARD_ID, default=BCU_APP): cv.string,
}
)
async def start_charge_session(service_call: ServiceCall) -> None:
"""Start a charge session with the provided device and charge card ID."""
# When no charge card is provided, use the default charge card set in the config flow.
charging_card_id = service_call.data[CHARGING_CARD_ID]
device_id = service_call.data[CONF_DEVICE_ID]
# Get the device based on the given device ID.
device = dr.async_get(service_call.hass).devices.get(device_id)
if device is None:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="invalid_device_id"
)
blue_current_config_entry: ConfigEntry | None = None
for config_entry_id in device.config_entries:
config_entry = service_call.hass.config_entries.async_get_entry(config_entry_id)
if not config_entry or config_entry.domain != DOMAIN:
# Not the blue_current config entry.
continue
if config_entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="config_entry_not_loaded"
)
blue_current_config_entry = config_entry
break
if not blue_current_config_entry:
# The device is not connected to a valid blue_current config entry.
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="no_config_entry"
)
connector = blue_current_config_entry.runtime_data
# Get the evse_id from the identifier of the device.
evse_id = next(
identifier[1] for identifier in device.identifiers if identifier[0] == DOMAIN
)
await connector.client.start_session(evse_id, charging_card_id)
@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Register the services."""
hass.services.async_register(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
start_charge_session,
SERVICE_START_CHARGE_SESSION_SCHEMA,
)

View File

@@ -11,7 +11,6 @@
"config_flow": true, "config_flow": true,
"dependencies": ["bluetooth_adapters"], "dependencies": ["bluetooth_adapters"],
"documentation": "https://www.home-assistant.io/integrations/bluemaestro", "documentation": "https://www.home-assistant.io/integrations/bluemaestro",
"integration_type": "device",
"iot_class": "local_push", "iot_class": "local_push",
"requirements": ["bluemaestro-ble==0.4.1"] "requirements": ["bluemaestro-ble==0.4.1"]
} }

View File

@@ -5,7 +5,6 @@
"codeowners": ["@thrawnarn", "@LouisChrist"], "codeowners": ["@thrawnarn", "@LouisChrist"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bluesound", "documentation": "https://www.home-assistant.io/integrations/bluesound",
"integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["pyblu==2.0.5"], "requirements": ["pyblu==2.0.5"],
"zeroconf": [ "zeroconf": [

View File

@@ -15,7 +15,7 @@
], ],
"quality_scale": "internal", "quality_scale": "internal",
"requirements": [ "requirements": [
"bleak==2.0.0", "bleak==1.0.1",
"bleak-retry-connector==4.4.3", "bleak-retry-connector==4.4.3",
"bluetooth-adapters==2.1.0", "bluetooth-adapters==2.1.0",
"bluetooth-auto-recovery==1.5.3", "bluetooth-auto-recovery==1.5.3",

View File

@@ -4,7 +4,6 @@
"codeowners": ["@gerard33", "@rikroe"], "codeowners": ["@gerard33", "@rikroe"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
"integration_type": "hub",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["bimmer_connected"], "loggers": ["bimmer_connected"],
"requirements": ["bimmer-connected[china]==0.17.3"] "requirements": ["bimmer-connected[china]==0.17.3"]

View File

@@ -14,7 +14,6 @@
} }
], ],
"documentation": "https://www.home-assistant.io/integrations/bond", "documentation": "https://www.home-assistant.io/integrations/bond",
"integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["bond_async"], "loggers": ["bond_async"],
"requirements": ["bond-async==0.2.1"], "requirements": ["bond-async==0.2.1"],

View File

@@ -5,7 +5,6 @@
"codeowners": ["@tschamm"], "codeowners": ["@tschamm"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/bosch_shc", "documentation": "https://www.home-assistant.io/integrations/bosch_shc",
"integration_type": "hub",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["boschshcpy"], "loggers": ["boschshcpy"],
"requirements": ["boschshcpy==0.2.107"], "requirements": ["boschshcpy==0.2.107"],

View File

@@ -4,7 +4,6 @@
"codeowners": ["@gjohansson-ST"], "codeowners": ["@gjohansson-ST"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/brottsplatskartan", "documentation": "https://www.home-assistant.io/integrations/brottsplatskartan",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["brottsplatskartan"], "loggers": ["brottsplatskartan"],
"requirements": ["brottsplatskartan==1.0.5"] "requirements": ["brottsplatskartan==1.0.5"]

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