mirror of
https://github.com/home-assistant/core.git
synced 2026-05-24 17:55:24 +02:00
Compare commits
141 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d53a8c7df9 | |||
| 8fe26bdf59 | |||
| f75d56c096 | |||
| 7e5942ae51 | |||
| f9ef9963e6 | |||
| cea718528f | |||
| 9c38868bbe | |||
| 62da1c34fb | |||
| 2d3a3bf4fc | |||
| daa60c6d55 | |||
| d45730aa02 | |||
| 05ef944766 | |||
| a51daf48c7 | |||
| 6a789d5af7 | |||
| ad4e218b69 | |||
| 55f576c784 | |||
| c7c3988b11 | |||
| ac825ca36d | |||
| 0b439a25e1 | |||
| 0385e81010 | |||
| 699fed7a3a | |||
| 0eefc8f327 | |||
| 6888d203eb | |||
| 359949adc2 | |||
| afe7d0cbbf | |||
| 2f7b3cb7d9 | |||
| c49ed549db | |||
| 3016198644 | |||
| f0c9156cdb | |||
| f091871aa5 | |||
| d25207180a | |||
| 9ce047b9be | |||
| 488c04fc5b | |||
| 7598fdb8cb | |||
| 49e22072c9 | |||
| c056242390 | |||
| 9cbb14bbde | |||
| 6634c4ce78 | |||
| ae1355666b | |||
| 2d0d202b80 | |||
| 9fd48344f8 | |||
| 7b4ed59861 | |||
| fb8f82542e | |||
| af5583ba76 | |||
| 2a943369d5 | |||
| 29425fd0ac | |||
| 271111fe75 | |||
| 37e9bdd36f | |||
| e1d1bdd377 | |||
| b3a60de487 | |||
| 0cb7ea5584 | |||
| 7bc7694e14 | |||
| c45c949080 | |||
| ec4f64172b | |||
| f88b7bcdf6 | |||
| 05009871aa | |||
| 4aa7323af2 | |||
| bcacf3a73c | |||
| 96a6babaef | |||
| e856271a5a | |||
| add023ed74 | |||
| 8d456cb24f | |||
| 5ebd95eb34 | |||
| 228d7189c3 | |||
| a8e141a48a | |||
| d42d52a0f7 | |||
| cee0fe071d | |||
| e3593c3076 | |||
| 5498de07ff | |||
| ac3f973d7d | |||
| 6b8a2a4032 | |||
| 74e40af4bb | |||
| 833e15d6f2 | |||
| ee56fd1eb0 | |||
| e6528bae8a | |||
| a17eb65498 | |||
| 912a839d66 | |||
| 4306863729 | |||
| ba2f66e751 | |||
| 94581d8ab6 | |||
| 7d6ec7fc58 | |||
| f49de3548e | |||
| 49ab42d3a2 | |||
| 383f6142f0 | |||
| 2f120cf604 | |||
| 37288849b3 | |||
| aa8659f507 | |||
| 40c0d79d1d | |||
| bef8632d78 | |||
| f00decfaa3 | |||
| 42e7add026 | |||
| 263aa3f16e | |||
| 03b364dcf0 | |||
| 3b1aaf39af | |||
| b82ba43fa4 | |||
| d81ef5593c | |||
| 5c5e50f024 | |||
| e796d9c467 | |||
| 342f23526f | |||
| 814ec697cf | |||
| 120f1446d4 | |||
| 170af75b7d | |||
| 5432d29489 | |||
| 8098f4f6bc | |||
| 6a70077687 | |||
| 5dbb0464ba | |||
| 1df165ea02 | |||
| 62542eb911 | |||
| a842cac34c | |||
| 2460f688e3 | |||
| a868ea443c | |||
| 1d8565483b | |||
| 1ef3301253 | |||
| 525952f016 | |||
| 3257275c5a | |||
| cb54fd4921 | |||
| b391fc61ea | |||
| fcd4e4939c | |||
| deb8b5da05 | |||
| c7754a6ce9 | |||
| 242724bd50 | |||
| 42454563db | |||
| bf03d0c216 | |||
| 568107e06b | |||
| 7da44428b6 | |||
| 0a27f31949 | |||
| 905b868c82 | |||
| 3187289913 | |||
| 87cecd4a44 | |||
| fed38b0e38 | |||
| 6a36d1260b | |||
| 49fc1b413d | |||
| bffb0417cc | |||
| 8b8c687fc3 | |||
| e3dd6b5fc5 | |||
| 94d620438b | |||
| 8867b792dc | |||
| 97967abfeb | |||
| af8fea272d | |||
| 2db0eed570 | |||
| ded1628c20 |
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/github-cli:1": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "ghcr.io/devcontainers/features/github-cli@sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671",
|
||||
"integrity": "sha256:d22f50b70ed75339b4eed1ba9ecde3a1791f90e88d37936517e3bace0bbad671"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ Dockerfile.dev linguist-language=Dockerfile
|
||||
# Generated files
|
||||
CODEOWNERS linguist-generated=true
|
||||
homeassistant/generated/*.py linguist-generated=true
|
||||
pylint/plugins/pylint_home_assistant/generated/*.py linguist-generated=true
|
||||
machine/* linguist-generated=true
|
||||
mypy.ini linguist-generated=true
|
||||
requirements.txt linguist-generated=true
|
||||
|
||||
@@ -25,6 +25,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
echo "::add-matcher::.github/workflows/matchers/check-executables-have-shebangs.json"
|
||||
echo "::add-matcher::.github/workflows/matchers/codespell.json"
|
||||
- name: Run prek
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
env:
|
||||
PREK_SKIP: no-commit-to-branch,mypy,pylint,gen_requirements_all,hassfest,hassfest-metadata,hassfest-mypy-config,zizmor
|
||||
RUFF_OUTPUT_FORMAT: github
|
||||
@@ -302,7 +302,7 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
uses: j178/prek-action@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -15,6 +15,7 @@ This repository contains the core of Home Assistant, a Python 3 based home autom
|
||||
|
||||
- When entering a new environment or worktree, run `script/setup` to set up the virtual environment with all development dependencies (pylint, pre-commit hooks, etc.). This is required before committing.
|
||||
- .vscode/tasks.json contains useful commands used for development.
|
||||
- After finishing a code session, run `uv run prek run --all-files` to check for linting and formatting issues.
|
||||
|
||||
## Python Syntax Notes
|
||||
|
||||
|
||||
Generated
+2
-4
@@ -236,8 +236,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/blebox/ @bbx-a @swistakm @bkobus-bbx
|
||||
/homeassistant/components/blink/ @fronzbot
|
||||
/tests/components/blink/ @fronzbot
|
||||
/homeassistant/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @NickKoepr @jtodorova23
|
||||
/homeassistant/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/tests/components/blue_current/ @gleeuwen @jtodorova23
|
||||
/homeassistant/components/bluemaestro/ @bdraco
|
||||
/tests/components/bluemaestro/ @bdraco
|
||||
/homeassistant/components/blueprint/ @home-assistant/core
|
||||
@@ -945,8 +945,6 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/knx/ @Julius2342 @farmio @marvin-w
|
||||
/homeassistant/components/kodi/ @OnFreund
|
||||
/tests/components/kodi/ @OnFreund
|
||||
/homeassistant/components/konnected/ @heythisisnate
|
||||
/tests/components/konnected/ @heythisisnate
|
||||
/homeassistant/components/kostal_plenticore/ @stegm
|
||||
/tests/components/kostal_plenticore/ @stegm
|
||||
/homeassistant/components/kraken/ @eifinger
|
||||
|
||||
@@ -134,7 +134,7 @@ class AuthManagerFlowManager(
|
||||
"""
|
||||
flow = cast(LoginFlow, flow)
|
||||
|
||||
if result["type"] != FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not FlowResultType.CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"service": "mdi:dialpad"
|
||||
},
|
||||
"alarm_toggle_chime": {
|
||||
"service": "mdi:abc"
|
||||
"service": "mdi:bell-ring"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CLOUD_NEVER_EXPOSED_ENTITIES,
|
||||
CONF_DESCRIPTION,
|
||||
CONF_NAME,
|
||||
UnitOfTemperature,
|
||||
@@ -373,9 +372,6 @@ def async_get_entities(
|
||||
"""Return all entities that are supported by Alexa."""
|
||||
entities: list[AlexaEntity] = []
|
||||
for state in hass.states.async_all():
|
||||
if state.entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
continue
|
||||
|
||||
if state.domain not in ENTITY_ADAPTERS:
|
||||
continue
|
||||
|
||||
|
||||
@@ -91,7 +91,6 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-translation-key-missing
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
|
||||
@@ -102,6 +102,9 @@
|
||||
"entry_not_loaded": {
|
||||
"message": "Entry not loaded: {entry}"
|
||||
},
|
||||
"invalid_auth": {
|
||||
"message": "Invalid authentication credentials: {error}"
|
||||
},
|
||||
"invalid_device_id": {
|
||||
"message": "Invalid device ID specified: {device_id}"
|
||||
},
|
||||
|
||||
@@ -226,7 +226,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Set initial options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
hass_apis: list[SelectOptionDict] = [
|
||||
|
||||
@@ -89,7 +89,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
def supported_features(self) -> WaterHeaterEntityFeature:
|
||||
"""Return the list of supported features."""
|
||||
supports_vacation_mode = any(
|
||||
supported_mode.mode == AOSmithOperationMode.VACATION
|
||||
supported_mode.mode is AOSmithOperationMode.VACATION
|
||||
for supported_mode in self.device.supported_modes
|
||||
)
|
||||
|
||||
@@ -122,7 +122,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
|
||||
@property
|
||||
def is_away_mode_on(self) -> bool:
|
||||
"""Return True if away mode is on."""
|
||||
return self.device.status.current_mode == AOSmithOperationMode.VACATION
|
||||
return self.device.status.current_mode is AOSmithOperationMode.VACATION
|
||||
|
||||
async def async_set_operation_mode(self, operation_mode: str) -> None:
|
||||
"""Set new target operation mode."""
|
||||
|
||||
@@ -369,7 +369,7 @@ class AppleTVManager(DeviceListener):
|
||||
|
||||
attrs[ATTR_MODEL] = (
|
||||
dev_info.raw_model
|
||||
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
|
||||
else model_str(dev_info.model)
|
||||
)
|
||||
attrs[ATTR_SW_VERSION] = dev_info.version
|
||||
|
||||
@@ -63,7 +63,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
|
||||
# Listen to keyboard updates
|
||||
atv.keyboard.listener = self
|
||||
# Set initial state based on current focus state
|
||||
self._update_state(atv.keyboard.text_focus_state == KeyboardFocusState.Focused)
|
||||
self._update_state(atv.keyboard.text_focus_state is KeyboardFocusState.Focused)
|
||||
|
||||
@callback
|
||||
def async_device_disconnected(self) -> None:
|
||||
@@ -78,7 +78,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
|
||||
|
||||
This is a callback function from pyatv.interface.KeyboardListener.
|
||||
"""
|
||||
self._update_state(new_state == KeyboardFocusState.Focused)
|
||||
self._update_state(new_state is KeyboardFocusState.Focused)
|
||||
|
||||
def _update_state(self, new_state: bool) -> None:
|
||||
"""Update and report."""
|
||||
|
||||
@@ -354,7 +354,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"name": self.atv.name,
|
||||
"type": (
|
||||
dev_info.raw_model
|
||||
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
|
||||
else model_str(dev_info.model)
|
||||
),
|
||||
}
|
||||
@@ -441,12 +441,12 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return await self.async_step_password()
|
||||
|
||||
# Figure out, depending on protocol, what kind of pairing is needed
|
||||
if service.pairing == PairingRequirement.Unsupported:
|
||||
if service.pairing is PairingRequirement.Unsupported:
|
||||
_LOGGER.debug("%s does not support pairing", self.protocol)
|
||||
return await self.async_pair_next_protocol()
|
||||
if service.pairing == PairingRequirement.Disabled:
|
||||
if service.pairing is PairingRequirement.Disabled:
|
||||
return await self.async_step_protocol_disabled()
|
||||
if service.pairing == PairingRequirement.NotNeeded:
|
||||
if service.pairing is PairingRequirement.NotNeeded:
|
||||
_LOGGER.debug("%s does not require pairing", self.protocol)
|
||||
self.credentials[self.protocol.value] = None
|
||||
return await self.async_pair_next_protocol()
|
||||
@@ -457,7 +457,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
pair_args: dict[str, Any] = {}
|
||||
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
|
||||
pair_args["name"] = "Home Assistant"
|
||||
if self.protocol == Protocol.DMAP:
|
||||
if self.protocol is Protocol.DMAP:
|
||||
pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
|
||||
|
||||
# Initiate the pairing process
|
||||
|
||||
@@ -139,7 +139,7 @@ class AppleTvMediaPlayer(
|
||||
all_features = atv.features.all_features()
|
||||
for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items():
|
||||
feature_info = all_features.get(feature_name)
|
||||
if feature_info and feature_info.state != FeatureState.Unsupported:
|
||||
if feature_info and feature_info.state is not FeatureState.Unsupported:
|
||||
self._attr_supported_features |= support_flag
|
||||
|
||||
# No need to schedule state update here as that will happen when the first
|
||||
@@ -188,14 +188,14 @@ class AppleTvMediaPlayer(
|
||||
return MediaPlayerState.OFF
|
||||
if (
|
||||
self._is_feature_available(FeatureName.PowerState)
|
||||
and self.atv.power.power_state == PowerState.Off
|
||||
and self.atv.power.power_state is PowerState.Off
|
||||
):
|
||||
return MediaPlayerState.OFF
|
||||
if self._playing:
|
||||
state = self._playing.device_state
|
||||
if state in (DeviceState.Idle, DeviceState.Loading):
|
||||
return MediaPlayerState.IDLE
|
||||
if state == DeviceState.Playing:
|
||||
if state is DeviceState.Playing:
|
||||
return MediaPlayerState.PLAYING
|
||||
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
|
||||
return MediaPlayerState.PAUSED
|
||||
@@ -446,7 +446,7 @@ class AppleTvMediaPlayer(
|
||||
def shuffle(self) -> bool | None:
|
||||
"""Boolean if shuffle is enabled."""
|
||||
if self._playing and self._is_feature_available(FeatureName.Shuffle):
|
||||
return self._playing.shuffle != ShuffleState.Off
|
||||
return self._playing.shuffle is not ShuffleState.Off
|
||||
return None
|
||||
|
||||
def _is_feature_available(self, feature: FeatureName) -> bool:
|
||||
@@ -506,7 +506,7 @@ class AppleTvMediaPlayer(
|
||||
and (self._is_feature_available(FeatureName.TurnOff))
|
||||
and (
|
||||
not self._is_feature_available(FeatureName.PowerState)
|
||||
or self.atv.power.power_state == PowerState.On
|
||||
or self.atv.power.power_state is PowerState.On
|
||||
)
|
||||
):
|
||||
await self.atv.power.turn_off()
|
||||
|
||||
@@ -59,7 +59,7 @@ def _check_keyboard_focus(atv: AppleTVInterface) -> None:
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_available",
|
||||
) from err
|
||||
if focus_state != KeyboardFocusState.Focused:
|
||||
if focus_state is not KeyboardFocusState.Focused:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_focused",
|
||||
|
||||
@@ -16,6 +16,13 @@ from homeassistant.helpers.service_info.ssdp import ATTR_UPNP_UDN, SsdpServiceIn
|
||||
|
||||
from .const import DEFAULT_NAME, DEFAULT_PORT, DOMAIN
|
||||
|
||||
STEP_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle config flow."""
|
||||
@@ -31,13 +38,22 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host, CONF_PORT: port})
|
||||
|
||||
async def _async_try_connect(self, host: str, port: int) -> None:
|
||||
"""Verify the device is reachable."""
|
||||
async def _async_try_connect(self, host: str, port: int) -> dict[str, str]:
|
||||
"""Verify the device is reachable; return errors keyed by reason."""
|
||||
client = Client(host, port)
|
||||
try:
|
||||
await client.start()
|
||||
except socket.gaierror:
|
||||
return {"base": "invalid_host"}
|
||||
except TimeoutError:
|
||||
return {"base": "timeout_connect"}
|
||||
except ConnectionRefusedError:
|
||||
return {"base": "connection_refused"}
|
||||
except ConnectionFailed, OSError:
|
||||
return {"base": "cannot_connect"}
|
||||
finally:
|
||||
await client.stop()
|
||||
return {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -53,19 +69,10 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
user_input[CONF_HOST], user_input[CONF_PORT], uuid
|
||||
)
|
||||
|
||||
try:
|
||||
await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
except socket.gaierror:
|
||||
errors["base"] = "invalid_host"
|
||||
except TimeoutError:
|
||||
errors["base"] = "timeout_connect"
|
||||
except ConnectionRefusedError:
|
||||
errors["base"] = "connection_refused"
|
||||
except ConnectionFailed, OSError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
errors = await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"{DEFAULT_NAME} ({user_input[CONF_HOST]})",
|
||||
data={
|
||||
@@ -74,16 +81,46 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
fields = {
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
|
||||
}
|
||||
schema = vol.Schema(fields)
|
||||
schema = STEP_DATA_SCHEMA
|
||||
if user_input is not None:
|
||||
schema = self.add_suggested_values_to_schema(schema, user_input)
|
||||
|
||||
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration of an existing entry."""
|
||||
errors: dict[str, str] = {}
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
if user_input is not None:
|
||||
uuid = await get_uniqueid_from_host(
|
||||
async_get_clientsession(self.hass), user_input[CONF_HOST]
|
||||
)
|
||||
if uuid:
|
||||
await self.async_set_unique_id(uuid)
|
||||
self._abort_if_unique_id_mismatch()
|
||||
|
||||
errors = await self._async_try_connect(
|
||||
user_input[CONF_HOST], user_input[CONF_PORT]
|
||||
)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={
|
||||
CONF_HOST: user_input[CONF_HOST],
|
||||
CONF_PORT: user_input[CONF_PORT],
|
||||
},
|
||||
)
|
||||
|
||||
schema = self.add_suggested_values_to_schema(
|
||||
STEP_DATA_SCHEMA, user_input or reconfigure_entry.data
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure", data_schema=schema, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -113,9 +150,7 @@ class ArcamFmjFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
await self._async_set_unique_id_and_update(host, port, uuid)
|
||||
|
||||
try:
|
||||
await self._async_try_connect(host, port)
|
||||
except ConnectionFailed, OSError:
|
||||
if await self._async_try_connect(host, port):
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
self.host = host
|
||||
|
||||
@@ -263,9 +263,9 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
source = self._state.get_source()
|
||||
if source == SourceCodes.DAB:
|
||||
if source is SourceCodes.DAB:
|
||||
value = self._state.get_dab_station()
|
||||
elif source == SourceCodes.FM:
|
||||
elif source is SourceCodes.FM:
|
||||
value = self._state.get_rds_information()
|
||||
else:
|
||||
value = None
|
||||
@@ -274,7 +274,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media, music track only."""
|
||||
if self._state.get_source() == SourceCodes.DAB:
|
||||
if self._state.get_source() is SourceCodes.DAB:
|
||||
value = self._state.get_dls_pdt()
|
||||
else:
|
||||
value = None
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]",
|
||||
"unique_id_mismatch": "Please ensure you reconfigure against the same device."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -16,6 +18,13 @@
|
||||
"confirm": {
|
||||
"description": "Do you want to add Arcam FMJ on `{host}` to Home Assistant?"
|
||||
},
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"port": "[%key:common::config_flow::data::port%]"
|
||||
},
|
||||
"description": "[%key:component::arcam_fmj::config::step::user::description%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
|
||||
@@ -1355,7 +1355,7 @@ class PipelineRun:
|
||||
) -> bool:
|
||||
"""Return true if all targeted entities were in the same area as the device."""
|
||||
if (
|
||||
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
|
||||
intent_response.response_type is not intent.IntentResponseType.ACTION_DONE
|
||||
or not intent_response.matched_states
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -9,12 +9,11 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT
|
||||
from homeassistant.const import ATTR_MODEL, ATTR_SERIAL_NUMBER, CONF_ADDRESS, CONF_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import (
|
||||
ATTR_FIRMWARE,
|
||||
ATTR_MODEL,
|
||||
DEFAULT_ADDRESS,
|
||||
DEFAULT_INTEGRATION_TITLE,
|
||||
DOMAIN,
|
||||
|
||||
@@ -19,8 +19,4 @@ DEVICES = "devices"
|
||||
MANUFACTURER = "ABB"
|
||||
|
||||
ATTR_DEVICE_NAME = "device_name"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_DEVICE_ID = "device_id"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL = "model"
|
||||
ATTR_FIRMWARE = "firmware"
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
ATTR_SERIAL_NUMBER,
|
||||
EntityCategory,
|
||||
UnitOfElectricCurrent,
|
||||
@@ -31,7 +32,6 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from .const import (
|
||||
ATTR_DEVICE_NAME,
|
||||
ATTR_FIRMWARE,
|
||||
ATTR_MODEL,
|
||||
DEFAULT_DEVICE_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
|
||||
@@ -251,12 +251,12 @@ class AuthProvidersView(HomeAssistantView):
|
||||
|
||||
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return {
|
||||
key: val for key, val in result.items() if key not in ("result", "data")
|
||||
}
|
||||
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = dict(result)
|
||||
@@ -289,11 +289,11 @@ class LoginFlowBaseView(HomeAssistantView):
|
||||
result: AuthFlowResult,
|
||||
) -> web.Response:
|
||||
"""Convert the flow result to a response."""
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
# @log_invalid_auth does not work here since it returns HTTP 200.
|
||||
# We need to manually log failed login attempts.
|
||||
if (
|
||||
result["type"] == data_entry_flow.FlowResultType.FORM
|
||||
result["type"] is data_entry_flow.FlowResultType.FORM
|
||||
and (errors := result.get("errors"))
|
||||
and errors.get("base")
|
||||
in (
|
||||
|
||||
@@ -142,9 +142,9 @@ def websocket_depose_mfa(
|
||||
|
||||
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
|
||||
"""Convert result to JSON serializable dict."""
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return dict(result)
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = dict(result)
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import axis
|
||||
from axis.errors import Unauthorized
|
||||
from axis.interfaces.mqtt import mqtt_json_to_event
|
||||
from axis.models.mqtt import ClientState
|
||||
from axis.models.mqtt import ClientState, mqtt_json_to_event
|
||||
from axis.stream_manager import Signal, State
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["axis"],
|
||||
"requirements": ["axis==71"],
|
||||
"requirements": ["axis==72"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "AXIS"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"domain": "blue_current",
|
||||
"name": "Blue Current",
|
||||
"codeowners": ["@gleeuwen", "@NickKoepr", "@jtodorova23"],
|
||||
"codeowners": ["@gleeuwen", "@jtodorova23"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/blue_current",
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -11,6 +11,7 @@ from bluetooth_adapters import (
|
||||
ADAPTER_CONNECTION_SLOTS,
|
||||
ADAPTER_HW_VERSION,
|
||||
ADAPTER_MANUFACTURER,
|
||||
ADAPTER_PASSIVE_SCAN,
|
||||
ADAPTER_SW_VERSION,
|
||||
DEFAULT_ADDRESS,
|
||||
DEFAULT_CONNECTION_SLOTS,
|
||||
@@ -69,6 +70,7 @@ from .api import (
|
||||
async_register_callback,
|
||||
async_register_scanner,
|
||||
async_remove_scanner,
|
||||
async_request_active_scan,
|
||||
async_scanner_by_source,
|
||||
async_scanner_count,
|
||||
async_scanner_devices_by_address,
|
||||
@@ -79,7 +81,6 @@ from .const import (
|
||||
BLUETOOTH_DISCOVERY_COOLDOWN_SECONDS,
|
||||
CONF_ADAPTER,
|
||||
CONF_DETAILS,
|
||||
CONF_PASSIVE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
CONF_SOURCE_DEVICE_ID,
|
||||
CONF_SOURCE_DOMAIN,
|
||||
@@ -93,7 +94,7 @@ from .manager import HomeAssistantBluetoothManager
|
||||
from .match import BluetoothCallbackMatcher, IntegrationMatcher
|
||||
from .models import BluetoothCallback, BluetoothChange
|
||||
from .storage import BluetoothStorage
|
||||
from .util import adapter_title
|
||||
from .util import adapter_title, resolve_scanning_mode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
@@ -128,6 +129,7 @@ __all__ = [
|
||||
"async_register_callback",
|
||||
"async_register_scanner",
|
||||
"async_remove_scanner",
|
||||
"async_request_active_scan",
|
||||
"async_scanner_by_source",
|
||||
"async_scanner_count",
|
||||
"async_scanner_devices_by_address",
|
||||
@@ -387,12 +389,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Bluetooth adapter {adapter} with address {address} not found"
|
||||
)
|
||||
passive = entry.options.get(CONF_PASSIVE)
|
||||
adapters = await manager.async_get_bluetooth_adapters()
|
||||
mode = BluetoothScanningMode.PASSIVE if passive else BluetoothScanningMode.ACTIVE
|
||||
details = adapters[adapter]
|
||||
mode = resolve_scanning_mode(entry.options)
|
||||
# AUTO needs passive scanning support to flip on demand; without it
|
||||
# the scanner would start passive on hardware that can't do passive.
|
||||
if mode is BluetoothScanningMode.AUTO and not details.get(ADAPTER_PASSIVE_SCAN):
|
||||
mode = BluetoothScanningMode.ACTIVE
|
||||
scanner = HaScanner(mode, adapter, address)
|
||||
scanner.async_setup()
|
||||
details = adapters[adapter]
|
||||
if entry.title == address:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, title=adapter_title(adapter, details)
|
||||
|
||||
@@ -68,9 +68,20 @@ class ActiveBluetoothProcessorCoordinator[_DataT](
|
||||
| None = None,
|
||||
poll_debouncer: Debouncer[Coroutine[Any, Any, None]] | None = None,
|
||||
connectable: bool = True,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the processor."""
|
||||
super().__init__(hass, logger, address, mode, update_method, connectable)
|
||||
super().__init__(
|
||||
hass,
|
||||
logger,
|
||||
address,
|
||||
mode,
|
||||
update_method,
|
||||
connectable,
|
||||
scan_interval,
|
||||
scan_duration,
|
||||
)
|
||||
|
||||
self._needs_poll_method = needs_poll_method
|
||||
self._poll_method = poll_method
|
||||
|
||||
@@ -130,17 +130,26 @@ def async_register_callback(
|
||||
callback: BluetoothCallback,
|
||||
match_dict: BluetoothCallbackMatcher | None,
|
||||
mode: BluetoothScanningMode,
|
||||
*,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Register to receive a callback on bluetooth change.
|
||||
|
||||
mode is currently not used as we only support active scanning.
|
||||
Passive scanning will be available in the future. The flag
|
||||
is required to be present to avoid a future breaking change
|
||||
when we support passive scanning.
|
||||
When ``mode`` is not PASSIVE and ``match_dict["address"]`` is set,
|
||||
the address is registered with habluetooth's active-scan scheduler
|
||||
so AUTO-mode scanners flip ACTIVE on demand for that device.
|
||||
``scan_interval`` / ``scan_duration`` default to habluetooth's
|
||||
DEFAULT_ACTIVE_SCAN_* (5 minutes / 10 seconds) when not provided;
|
||||
integrations that need a different cadence can pass explicit
|
||||
values. Without an address in the matcher the active-scan request
|
||||
is skipped; the callback itself still fires normally.
|
||||
|
||||
Returns a callback that can be used to cancel the registration.
|
||||
"""
|
||||
return _get_manager(hass).async_register_callback(callback, match_dict)
|
||||
return _get_manager(hass).async_register_callback(
|
||||
callback, match_dict, mode, scan_interval, scan_duration
|
||||
)
|
||||
|
||||
|
||||
async def async_process_advertisements(
|
||||
@@ -161,7 +170,7 @@ async def async_process_advertisements(
|
||||
done.set_result(service_info)
|
||||
|
||||
unload = _get_manager(hass).async_register_callback(
|
||||
_async_discovered_device, match_dict
|
||||
_async_discovered_device, match_dict, mode, scan_duration=timeout
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -275,3 +284,19 @@ def async_set_fallback_availability_interval(
|
||||
) -> None:
|
||||
"""Override the fallback availability timeout for a MAC address."""
|
||||
_get_manager(hass).async_set_fallback_availability_interval(address, interval)
|
||||
|
||||
|
||||
async def async_request_active_scan(
|
||||
hass: HomeAssistant, duration: float | None = None
|
||||
) -> None:
|
||||
"""Run an on-demand active sweep across every AUTO scanner.
|
||||
|
||||
Intended for config-flow discovery and other one-shot probes that
|
||||
need fresh advertisements without waiting for the periodic
|
||||
rediscovery cadence. Awaits ``duration`` seconds so the caller can
|
||||
then read newly discovered advertisements. Defaults to habluetooth's
|
||||
on-demand sweep duration when ``duration`` is not provided; the
|
||||
scheduler clamps the value to its allowed range. Concurrent callers
|
||||
dedupe to a single bus-wide window.
|
||||
"""
|
||||
await _get_manager(hass).async_request_active_scan(duration)
|
||||
|
||||
@@ -12,7 +12,7 @@ from bluetooth_adapters import (
|
||||
adapter_model,
|
||||
get_adapters,
|
||||
)
|
||||
from habluetooth import get_manager
|
||||
from habluetooth import BluetoothScanningMode, get_manager
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import onboarding
|
||||
@@ -24,14 +24,21 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaFlowFormStep,
|
||||
SchemaOptionsFlowHandler,
|
||||
)
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.typing import DiscoveryInfoType
|
||||
|
||||
from .const import (
|
||||
CONF_ADAPTER,
|
||||
CONF_DETAILS,
|
||||
CONF_MODE,
|
||||
CONF_PASSIVE,
|
||||
CONF_SOURCE,
|
||||
CONF_SOURCE_CONFIG_ENTRY_ID,
|
||||
@@ -40,15 +47,39 @@ from .const import (
|
||||
CONF_SOURCE_MODEL,
|
||||
DOMAIN,
|
||||
)
|
||||
from .util import adapter_title
|
||||
from .util import adapter_title, resolve_scanning_mode
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSIVE, default=False): bool,
|
||||
}
|
||||
_MODE_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
BluetoothScanningMode.AUTO.value,
|
||||
BluetoothScanningMode.ACTIVE.value,
|
||||
BluetoothScanningMode.PASSIVE.value,
|
||||
],
|
||||
translation_key="mode",
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def _options_schema(handler: SchemaCommonFlowHandler) -> vol.Schema:
|
||||
"""Build the options schema with the saved mode as the default."""
|
||||
current = resolve_scanning_mode(handler.options).value
|
||||
return vol.Schema({vol.Required(CONF_MODE, default=current): _MODE_SELECTOR})
|
||||
|
||||
|
||||
async def _validate_options(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Mirror CONF_MODE into the legacy CONF_PASSIVE for downgrade safety."""
|
||||
user_input[CONF_PASSIVE] = (
|
||||
user_input[CONF_MODE] == BluetoothScanningMode.PASSIVE.value
|
||||
)
|
||||
return user_input
|
||||
|
||||
|
||||
OPTIONS_FLOW = {
|
||||
"init": SchemaFlowFormStep(OPTIONS_SCHEMA),
|
||||
"init": SchemaFlowFormStep(_options_schema, validate_user_input=_validate_options),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,14 +7,21 @@ from habluetooth import ( # noqa: F401
|
||||
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
|
||||
SCANNER_WATCHDOG_INTERVAL,
|
||||
SCANNER_WATCHDOG_TIMEOUT,
|
||||
BluetoothScanningMode,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_MODE # noqa: F401
|
||||
|
||||
DOMAIN = "bluetooth"
|
||||
|
||||
CONF_ADAPTER = "adapter"
|
||||
CONF_DETAILS = "details"
|
||||
# CONF_PASSIVE is the legacy boolean option; we keep writing it alongside
|
||||
# CONF_MODE so a downgrade to a pre-AUTO release reads a sensible value.
|
||||
CONF_PASSIVE = "passive"
|
||||
|
||||
DEFAULT_MODE = BluetoothScanningMode.AUTO.value
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_SOURCE: Final = "source"
|
||||
|
||||
@@ -202,6 +202,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
self,
|
||||
callback: BluetoothCallback,
|
||||
matcher: BluetoothCallbackMatcher | None,
|
||||
mode: BluetoothScanningMode = BluetoothScanningMode.ACTIVE,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> Callable[[], None]:
|
||||
"""Register a callback."""
|
||||
callback_matcher = BluetoothCallbackMatcherWithCallback(callback=callback)
|
||||
@@ -216,15 +219,31 @@ class HomeAssistantBluetoothManager(BluetoothManager):
|
||||
connectable = callback_matcher[CONNECTABLE]
|
||||
self._callback_index.add_callback_matcher(callback_matcher)
|
||||
|
||||
# If the matcher targets a specific address and the caller
|
||||
# didn't explicitly ask for PASSIVE, wire it into habluetooth's
|
||||
# active-scan scheduler so AUTO-mode scanners flip ACTIVE on
|
||||
# demand for this device. ``scan_interval``/``scan_duration``
|
||||
# default to habluetooth's DEFAULT_ACTIVE_SCAN_* when None.
|
||||
cancel_active_scan: Callable[[], None] | None = None
|
||||
if (
|
||||
mode is not BluetoothScanningMode.PASSIVE
|
||||
and (address := callback_matcher.get(ADDRESS)) is not None
|
||||
):
|
||||
cancel_active_scan = self.async_register_active_scan(
|
||||
address, scan_interval, scan_duration
|
||||
)
|
||||
|
||||
def _async_remove_callback() -> None:
|
||||
self._callback_index.remove_callback_matcher(callback_matcher)
|
||||
if cancel_active_scan is not None:
|
||||
cancel_active_scan()
|
||||
|
||||
# If we have history for the subscriber, we can trigger the callback
|
||||
# immediately with the last packet so the subscriber can see the
|
||||
# device.
|
||||
history = self._connectable_history if connectable else self._all_history
|
||||
service_infos: Iterable[BluetoothServiceInfoBleak] = []
|
||||
if address := callback_matcher.get(ADDRESS):
|
||||
if (address := callback_matcher.get(ADDRESS)) is not None:
|
||||
if service_info := history.get(address):
|
||||
service_infos = [service_info]
|
||||
else:
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
"bleak-retry-connector==4.6.0",
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.29.11",
|
||||
"dbus-fast==5.0.0",
|
||||
"habluetooth==6.1.0"
|
||||
"bluetooth-data-tools==1.29.18",
|
||||
"dbus-fast==5.0.3",
|
||||
"habluetooth==6.7.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -298,9 +298,13 @@ class PassiveBluetoothProcessorCoordinator[_DataT](BasePassiveBluetoothCoordinat
|
||||
mode: BluetoothScanningMode,
|
||||
update_method: Callable[[BluetoothServiceInfoBleak], _DataT],
|
||||
connectable: bool = False,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(hass, logger, address, mode, connectable)
|
||||
super().__init__(
|
||||
hass, logger, address, mode, connectable, scan_interval, scan_duration
|
||||
)
|
||||
self._processors: list[PassiveBluetoothDataProcessor[Any, _DataT]] = []
|
||||
self._update_method = update_method
|
||||
self.last_update_success = True
|
||||
|
||||
@@ -48,9 +48,21 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"passive": "Passive scanning"
|
||||
"mode": "Scanning mode"
|
||||
},
|
||||
"data_description": {
|
||||
"mode": "Auto is recommended for most setups. It saves battery on your Bluetooth devices while still catching new devices and updates quickly."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"mode": {
|
||||
"options": {
|
||||
"active": "Active (uses more device battery, fastest updates)",
|
||||
"auto": "Auto (recommended, saves device battery)",
|
||||
"passive": "Passive (lowest device battery use, some details may be missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
address: str,
|
||||
mode: BluetoothScanningMode,
|
||||
connectable: bool,
|
||||
scan_interval: float | None = None,
|
||||
scan_duration: float | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
self.hass = hass
|
||||
@@ -38,6 +40,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
self.connectable = connectable
|
||||
self._on_stop: list[CALLBACK_TYPE] = []
|
||||
self.mode = mode
|
||||
self._scan_interval = scan_interval
|
||||
self._scan_duration = scan_duration
|
||||
self._last_unavailable_time = 0.0
|
||||
self._last_name = address
|
||||
# Subclasses are responsible for setting _available to True
|
||||
@@ -92,6 +96,8 @@ class BasePassiveBluetoothCoordinator(ABC):
|
||||
address=self.address, connectable=self.connectable
|
||||
),
|
||||
self.mode,
|
||||
scan_interval=self._scan_interval,
|
||||
scan_duration=self._scan_duration,
|
||||
)
|
||||
)
|
||||
self._on_stop.append(
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
"""The bluetooth integration utilities."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bluetooth_adapters import (
|
||||
ADAPTER_ADDRESS,
|
||||
ADAPTER_MANUFACTURER,
|
||||
@@ -9,14 +13,32 @@ from bluetooth_adapters import (
|
||||
adapter_unique_name,
|
||||
)
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
from habluetooth import get_manager
|
||||
from habluetooth import BluetoothScanningMode, get_manager
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
from .const import CONF_MODE, CONF_PASSIVE, DEFAULT_MODE
|
||||
from .models import BluetoothServiceInfoBleak
|
||||
from .storage import BluetoothStorage
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def resolve_scanning_mode(options: Mapping[str, Any]) -> BluetoothScanningMode:
|
||||
"""Resolve CONF_MODE, falling back to legacy CONF_PASSIVE or DEFAULT_MODE."""
|
||||
if (mode_value := options.get(CONF_MODE)) is not None:
|
||||
try:
|
||||
return BluetoothScanningMode(mode_value)
|
||||
except TypeError, ValueError:
|
||||
_LOGGER.warning("Unknown bluetooth scanning mode %r", mode_value)
|
||||
return BluetoothScanningMode(DEFAULT_MODE)
|
||||
if (legacy_passive := options.get(CONF_PASSIVE)) is True:
|
||||
return BluetoothScanningMode.PASSIVE
|
||||
if legacy_passive is False:
|
||||
return BluetoothScanningMode.ACTIVE
|
||||
return BluetoothScanningMode(DEFAULT_MODE)
|
||||
|
||||
|
||||
class InvalidConfigEntryID(HomeAssistantError):
|
||||
"""Invalid config entry id."""
|
||||
|
||||
@@ -273,7 +273,7 @@ async def ws_subscribe_scanner_details(
|
||||
|
||||
def _async_registration_changed(registration: HaScannerRegistration) -> None:
|
||||
added_event = HaScannerRegistrationEvent.ADDED
|
||||
event_type = "add" if registration.event == added_event else "remove"
|
||||
event_type = "add" if registration.event is added_event else "remove"
|
||||
_async_event_message({event_type: [registration.scanner.details]})
|
||||
|
||||
manager = _get_manager(hass)
|
||||
|
||||
@@ -9,7 +9,14 @@ from pybravia import BraviaAuthError, BraviaClient, BraviaError, BraviaNotSuppor
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_CLIENT_ID, CONF_HOST, CONF_MAC, CONF_NAME, CONF_PIN
|
||||
from homeassistant.const import (
|
||||
ATTR_MODEL,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_HOST,
|
||||
CONF_MAC,
|
||||
CONF_NAME,
|
||||
CONF_PIN,
|
||||
)
|
||||
from homeassistant.helpers import instance_id
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
from homeassistant.helpers.service_info.ssdp import (
|
||||
@@ -23,7 +30,6 @@ from homeassistant.util.network import is_host_valid
|
||||
from .const import (
|
||||
ATTR_CID,
|
||||
ATTR_MAC,
|
||||
ATTR_MODEL,
|
||||
CONF_NICKNAME,
|
||||
CONF_USE_PSK,
|
||||
CONF_USE_SSL,
|
||||
|
||||
@@ -6,8 +6,6 @@ from typing import Final
|
||||
ATTR_CID: Final = "cid"
|
||||
ATTR_MAC: Final = "macAddr"
|
||||
ATTR_MANUFACTURER: Final = "Sony"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL: Final = "model"
|
||||
|
||||
CONF_NICKNAME: Final = "nickname"
|
||||
CONF_USE_PSK: Final = "use_psk"
|
||||
|
||||
@@ -158,7 +158,10 @@ def process_service_info(
|
||||
)
|
||||
|
||||
# If payload is encrypted and the bindkey is not verified then we need to reauth
|
||||
if data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified:
|
||||
if (
|
||||
data.encryption_scheme is not EncryptionScheme.NONE
|
||||
and not data.bindkey_verified
|
||||
):
|
||||
entry.async_start_reauth(hass, data={"device": data})
|
||||
|
||||
return update
|
||||
|
||||
@@ -59,7 +59,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info = discovery_info
|
||||
self._discovered_device = device
|
||||
|
||||
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
@@ -125,7 +125,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info = discovery.discovery_info
|
||||
self._discovered_device = discovery.device
|
||||
|
||||
if discovery.device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if discovery.device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
|
||||
return self._async_get_or_create_entry()
|
||||
@@ -164,7 +164,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
self._discovery_info = device.last_service_info
|
||||
|
||||
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
|
||||
# Otherwise there wasn't actually encryption so abort
|
||||
|
||||
@@ -32,7 +32,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import Event, HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er, start
|
||||
@@ -275,9 +274,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
|
||||
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||
"""If an entity should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.alexa_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
@@ -308,8 +304,6 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
||||
"""If an entity should be exposed."""
|
||||
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||
if not entity_filter.empty_filter:
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_ALEXA, entity_id)
|
||||
|
||||
@@ -22,7 +22,6 @@ from homeassistant.components.homeassistant.exposed_entities import (
|
||||
async_should_expose,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import (
|
||||
CoreState,
|
||||
Event,
|
||||
@@ -282,9 +281,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
|
||||
def _should_expose_legacy(self, entity_id: str) -> bool:
|
||||
"""If an entity ID should be exposed."""
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
|
||||
entity_configs = self._prefs.google_entity_configs
|
||||
entity_config = entity_configs.get(entity_id, {})
|
||||
entity_expose: bool | None = entity_config.get(PREF_SHOULD_EXPOSE)
|
||||
@@ -316,8 +312,6 @@ class CloudGoogleConfig(AbstractConfig):
|
||||
"""If an entity should be exposed."""
|
||||
entity_filter: EntityFilter = self._config[CONF_FILTER]
|
||||
if not entity_filter.empty_filter:
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES:
|
||||
return False
|
||||
return entity_filter(entity_id)
|
||||
|
||||
return async_should_expose(self.hass, CLOUD_GOOGLE, entity_id)
|
||||
|
||||
@@ -29,7 +29,6 @@ from homeassistant.components.homeassistant import exposed_entities
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView, require_admin
|
||||
from homeassistant.components.http.data_validator import RequestDataValidator
|
||||
from homeassistant.components.system_health import get_info as get_system_health_info
|
||||
from homeassistant.const import CLOUD_NEVER_EXPOSED_ENTITIES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
@@ -973,7 +972,7 @@ async def google_assistant_get(
|
||||
return
|
||||
|
||||
entity = google_helpers.GoogleEntity(hass, gconf, state)
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity.is_supported():
|
||||
if not entity.is_supported():
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_SUPPORTED,
|
||||
@@ -1075,9 +1074,7 @@ async def alexa_get(
|
||||
"""Get data for a single alexa entity."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
|
||||
if entity_id in CLOUD_NEVER_EXPOSED_ENTITIES or not entity_supported_by_alexa(
|
||||
hass, entity_id
|
||||
):
|
||||
if not entity_supported_by_alexa(hass, entity_id):
|
||||
connection.send_error(
|
||||
msg["id"],
|
||||
websocket_api.ERR_NOT_SUPPORTED,
|
||||
|
||||
@@ -5,6 +5,3 @@ ATTR_URL = "color_extract_url"
|
||||
|
||||
DOMAIN = "color_extractor"
|
||||
DEFAULT_NAME = "Color extractor"
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
SERVICE_TURN_ON = "turn_on"
|
||||
|
||||
@@ -14,11 +14,11 @@ from homeassistant.components.light import (
|
||||
DOMAIN as LIGHT_DOMAIN,
|
||||
LIGHT_TURN_ON_SCHEMA,
|
||||
)
|
||||
from homeassistant.const import SERVICE_TURN_ON as LIGHT_SERVICE_TURN_ON
|
||||
from homeassistant.const import SERVICE_TURN_ON
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
|
||||
from .const import ATTR_PATH, ATTR_URL, DOMAIN, SERVICE_TURN_ON
|
||||
from .const import ATTR_PATH, ATTR_URL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -141,7 +141,7 @@ async def async_handle_service(service_call: ServiceCall) -> None:
|
||||
service_data[ATTR_RGB_COLOR] = color
|
||||
|
||||
await service_call.hass.services.async_call(
|
||||
LIGHT_DOMAIN, LIGHT_SERVICE_TURN_ON, service_data, blocking=True
|
||||
LIGHT_DOMAIN, SERVICE_TURN_ON, service_data, blocking=True
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
if self._area.human_status is AlarmAreaState.UNKNOWN:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -124,7 +124,7 @@ class ComelitAlarmEntity(
|
||||
self._area.human_status,
|
||||
self._area.armed,
|
||||
)
|
||||
if self._area.human_status == AlarmAreaState.ARMED:
|
||||
if self._area.human_status is AlarmAreaState.ARMED:
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
|
||||
return AlarmControlPanelState.ARMED_AWAY
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
|
||||
|
||||
@@ -43,7 +43,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: cast(ComelitVedoAreaObject, obj).anomaly,
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoAreaObject, obj).human_status != AlarmAreaState.UNKNOWN
|
||||
cast(ComelitVedoAreaObject, obj).human_status is not AlarmAreaState.UNKNOWN
|
||||
),
|
||||
),
|
||||
ComelitBinarySensorEntityDescription(
|
||||
@@ -67,7 +67,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
object_type=ALARM_ZONE,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status == AlarmZoneState.FAULTY
|
||||
cast(ComelitVedoZoneObject, obj).human_status is AlarmZoneState.FAULTY
|
||||
),
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status
|
||||
|
||||
@@ -65,11 +65,9 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
@@ -166,12 +166,12 @@ class ComelitVedoSensorEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Sensor availability."""
|
||||
return self._zone_object.human_status != AlarmZoneState.UNAVAILABLE
|
||||
return self._zone_object.human_status is not AlarmZoneState.UNAVAILABLE
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Sensor value."""
|
||||
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
|
||||
if (status := self._zone_object.human_status) is AlarmZoneState.UNKNOWN:
|
||||
return None
|
||||
|
||||
return cast(str, status.value)
|
||||
|
||||
@@ -148,7 +148,7 @@ def _prepare_config_flow_result_json(
|
||||
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return prepare_result_json(result)
|
||||
|
||||
data = {key: val for key, val in result.items() if key not in ("data", "context")}
|
||||
|
||||
@@ -646,7 +646,7 @@ class DefaultAgent(ConversationEntity):
|
||||
cache_value = self._intent_cache.get(cache_key)
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
cache_value.stage is IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
):
|
||||
_LOGGER.debug("Got cached result for exposed entities")
|
||||
return cache_value.result
|
||||
@@ -686,7 +686,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unexposed_entities_match = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
cache_value.stage is IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for all entities")
|
||||
return cache_value.result
|
||||
@@ -731,7 +731,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unknown_names = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES
|
||||
cache_value.stage is IntentMatchingStage.UNKNOWN_NAMES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for unknown names")
|
||||
return cache_value.result
|
||||
@@ -1546,7 +1546,7 @@ def _get_match_error_response(
|
||||
# device_class only
|
||||
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
|
||||
|
||||
if (reason == intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
if (reason is intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
domain = next(iter(constraints.domains)) # first domain
|
||||
if constraints.area_name:
|
||||
# domain in area
|
||||
@@ -1565,7 +1565,7 @@ def _get_match_error_response(
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
if reason == intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if reason is intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if constraints.floor_name:
|
||||
# duplicate on floor
|
||||
return ErrorKey.DUPLICATE_ENTITIES_IN_FLOOR, {
|
||||
@@ -1582,26 +1582,26 @@ def _get_match_error_response(
|
||||
|
||||
return ErrorKey.DUPLICATE_ENTITIES, {"entity": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.INVALID_AREA:
|
||||
if reason is intent.MatchFailedReason.INVALID_AREA:
|
||||
# Invalid area name
|
||||
return ErrorKey.NO_AREA, {"area": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.INVALID_FLOOR:
|
||||
if reason is intent.MatchFailedReason.INVALID_FLOOR:
|
||||
# Invalid floor name
|
||||
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
|
||||
|
||||
if reason == intent.MatchFailedReason.FEATURE:
|
||||
if reason is intent.MatchFailedReason.FEATURE:
|
||||
# Feature not supported by entity
|
||||
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
|
||||
|
||||
if reason == intent.MatchFailedReason.STATE:
|
||||
if reason is intent.MatchFailedReason.STATE:
|
||||
# Entity is not in correct state
|
||||
assert constraints.states
|
||||
state = next(iter(constraints.states))
|
||||
|
||||
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
|
||||
|
||||
if reason == intent.MatchFailedReason.ASSISTANT:
|
||||
if reason is intent.MatchFailedReason.ASSISTANT:
|
||||
# Not exposed
|
||||
if constraints.name:
|
||||
if constraints.area_name:
|
||||
|
||||
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
|
||||
"""Implementing the Currencylayer sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by currencylayer.com"
|
||||
_attr_icon = "mdi:currency"
|
||||
_attr_icon = "mdi:currency-usd"
|
||||
|
||||
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DeconzConfigEntry
|
||||
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
|
||||
from .const import ATTR_OFFSET, ATTR_VALVE
|
||||
from .entity import DeconzDevice
|
||||
from .hub import DeconzHub
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
ATTR_DARK = "dark"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_LOCKED = "locked"
|
||||
ATTR_OFFSET = "offset"
|
||||
ATTR_ON = "on"
|
||||
ATTR_VALVE = "valve"
|
||||
|
||||
@@ -80,7 +80,7 @@ async def async_validate_device_automation_config(
|
||||
# the checks below which look for a config entry matching the device automation
|
||||
# domain
|
||||
if (
|
||||
automation_type == DeviceAutomationType.ACTION
|
||||
automation_type is DeviceAutomationType.ACTION
|
||||
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
|
||||
):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.1",
|
||||
"aiodiscover==3.2.0",
|
||||
"aiodiscover==3.2.3",
|
||||
"cached-ipaddress==1.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -281,7 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
await self._device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -290,7 +290,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
@@ -718,7 +718,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
# If already playing, or don't want to autoplay, no need to call Play
|
||||
autoplay = extra.get("autoplay", True)
|
||||
if self._device.transport_state == TransportState.PLAYING or not autoplay:
|
||||
if self._device.transport_state is TransportState.PLAYING or not autoplay:
|
||||
return
|
||||
|
||||
# Play it
|
||||
@@ -748,7 +748,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
|
||||
@@ -782,10 +782,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
if play_mode == PlayMode.REPEAT_ONE:
|
||||
if play_mode is PlayMode.REPEAT_ONE:
|
||||
return RepeatMode.ONE
|
||||
|
||||
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
|
||||
|
||||
@@ -236,7 +236,7 @@ class DmsDeviceSource:
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -258,7 +258,7 @@ class DmsDeviceSource:
|
||||
await self.device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -267,7 +267,7 @@ class DmsDeviceSource:
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
|
||||
@@ -6,7 +6,6 @@ import logging
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
from pycares import AresError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
@@ -78,11 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
errors = [
|
||||
result
|
||||
for result in results
|
||||
if isinstance(
|
||||
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
|
||||
)
|
||||
result for result in results if isinstance(result, (TimeoutError, DNSError))
|
||||
]
|
||||
if errors and len(errors) == len(results):
|
||||
await _close_resolvers()
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"download_dir": "Download directory"
|
||||
},
|
||||
"data_description": {
|
||||
"download_dir": "The directory where downloaded files will be stored. This can be an absolute path or a path relative to the Home Assistant configuration directory."
|
||||
},
|
||||
"description": "Select a location to get to store downloads. The setup will check if the directory exists."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,13 @@ from typing import TYPE_CHECKING, Any
|
||||
from dropmqttapi.discovery import DropDiscovery
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_DEVICE_ID
|
||||
from homeassistant.helpers.service_info.mqtt import MqttServiceInfo
|
||||
|
||||
from .const import (
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_DATA_TOPIC,
|
||||
CONF_DEVICE_DESC,
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_DEVICE_OWNER_ID,
|
||||
CONF_DEVICE_TYPE,
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
CONF_COMMAND_TOPIC = "drop_command_topic"
|
||||
CONF_DATA_TOPIC = "drop_data_topic"
|
||||
CONF_DEVICE_DESC = "device_desc"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_DEVICE_ID = "device_id"
|
||||
CONF_DEVICE_TYPE = "device_type"
|
||||
CONF_HUB_ID = "drop_hub_id"
|
||||
CONF_DEVICE_NAME = "name"
|
||||
|
||||
@@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
TEMPERATURE,
|
||||
EntityCategory,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
@@ -49,8 +50,6 @@ CURRENT_SYSTEM_PRESSURE = "current_system_pressure"
|
||||
HIGH_SYSTEM_PRESSURE = "high_system_pressure"
|
||||
LOW_SYSTEM_PRESSURE = "low_system_pressure"
|
||||
BATTERY = "battery"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
TEMPERATURE = "temperature"
|
||||
INLET_TDS = "inlet_tds"
|
||||
OUTLET_TDS = "outlet_tds"
|
||||
CARTRIDGE_1_LIFE = "cart1"
|
||||
|
||||
@@ -8,7 +8,7 @@ from elevenlabs.core import ApiError
|
||||
from httpx import ConnectError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODEL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import (
|
||||
ConfigEntryAuthFailed,
|
||||
@@ -17,7 +17,7 @@ from homeassistant.exceptions import (
|
||||
)
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
|
||||
from .const import CONF_MODEL, CONF_STT_MODEL
|
||||
from .const import CONF_STT_MODEL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from elevenlabs.core import ApiError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODEL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.httpx_client import get_async_client
|
||||
from homeassistant.helpers.selector import (
|
||||
@@ -20,7 +20,6 @@ from homeassistant.helpers.selector import (
|
||||
from . import ElevenLabsConfigEntry
|
||||
from .const import (
|
||||
CONF_CONFIGURE_VOICE,
|
||||
CONF_MODEL,
|
||||
CONF_SIMILARITY,
|
||||
CONF_STABILITY,
|
||||
CONF_STT_AUTO_LANGUAGE,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
"""Constants for the ElevenLabs text-to-speech integration."""
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODEL = "model"
|
||||
|
||||
CONF_VOICE = "voice"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_MODEL = "model"
|
||||
CONF_CONFIGURE_VOICE = "configure_voice"
|
||||
CONF_STABILITY = "stability"
|
||||
CONF_SIMILARITY = "similarity"
|
||||
|
||||
@@ -150,9 +150,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
|
||||
|
||||
raw_pcm_compatible = (
|
||||
metadata.codec == AudioCodecs.PCM
|
||||
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel == AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate == AudioBitRates.BITRATE_16
|
||||
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel is AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate is AudioBitRates.BITRATE_16
|
||||
)
|
||||
if raw_pcm_compatible:
|
||||
file_format = "pcm_s16le_16"
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.components.tts import (
|
||||
TtsAudioType,
|
||||
Voice,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.const import ATTR_MODEL, EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import ElevenLabsConfigEntry
|
||||
from .const import (
|
||||
ATTR_MODEL,
|
||||
CONF_SIMILARITY,
|
||||
CONF_STABILITY,
|
||||
CONF_STYLE,
|
||||
|
||||
@@ -50,5 +50,5 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
# Zone in NORMAL state is OFF; any other state is ON
|
||||
self._attr_is_on = bool(
|
||||
self._element.logical_status != ZoneLogicalStatus.NORMAL
|
||||
self._element.logical_status is not ZoneLogicalStatus.NORMAL
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
ThermostatMode.EMERGENCY_HEAT,
|
||||
):
|
||||
return self._element.heat_setpoint
|
||||
if self._element.mode == ThermostatMode.COOL:
|
||||
if self._element.mode is ThermostatMode.COOL:
|
||||
return self._element.cool_setpoint
|
||||
return None
|
||||
|
||||
@@ -162,6 +162,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode]
|
||||
if (
|
||||
self._attr_hvac_mode == HVACMode.OFF
|
||||
and self._element.fan == ThermostatFan.ON
|
||||
and self._element.fan is ThermostatFan.ON
|
||||
):
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
|
||||
@@ -56,7 +56,7 @@ class ElkNumberSetting(ElkAttachedEntity, NumberEntity):
|
||||
def __init__(self, element: Setting, elk: Any, elk_data: ELKM1Data) -> None:
|
||||
"""Initialize the number setting."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
if element.value_format == SettingFormat.TIMER:
|
||||
if element.value_format is SettingFormat.TIMER:
|
||||
self._attr_device_class = NumberDeviceClass.DURATION
|
||||
self._attr_native_unit_of_measurement = UnitOfTime.SECONDS
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
for setting in elk.settings:
|
||||
setting = cast(Setting, setting)
|
||||
domain = (
|
||||
"time" if setting.value_format == SettingFormat.TIME_OF_DAY else "number"
|
||||
"time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
|
||||
)
|
||||
|
||||
orig_unique_id = generate_unique_id(elk_data.prefix, setting)
|
||||
@@ -288,7 +288,7 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def temperature_unit(self) -> str | None:
|
||||
"""Return the temperature unit."""
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
return None
|
||||
|
||||
@@ -305,18 +305,18 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
if self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
if self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
return UnitOfElectricPotential.VOLT
|
||||
return None
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
self._attr_native_value = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPERATURE
|
||||
)
|
||||
elif self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
elif self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
self._attr_native_value = f"{self._element.voltage}"
|
||||
else:
|
||||
self._attr_native_value = pretty_const(self._element.logical_status.name)
|
||||
|
||||
@@ -66,7 +66,7 @@ class ElkThermostatEMHeat(ElkEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get the current emergency heat status."""
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
|
||||
return self._element.mode is ThermostatMode.EMERGENCY_HEAT
|
||||
|
||||
def _elk_set(self, mode: ThermostatMode) -> None:
|
||||
"""Set the thermostat mode."""
|
||||
|
||||
@@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
time_settings = [
|
||||
setting
|
||||
for setting in cast(list[Setting], elk.settings)
|
||||
if setting.value_format == SettingFormat.TIME_OF_DAY
|
||||
if setting.value_format is SettingFormat.TIME_OF_DAY
|
||||
]
|
||||
|
||||
create_elk_entities(
|
||||
|
||||
@@ -10,7 +10,7 @@ from aiohttp import ClientError, ClientResponseError
|
||||
from energyid_webhooks.client_v2 import WebhookClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.const import CONF_DEVICE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import (
|
||||
CALLBACK_TYPE,
|
||||
Event,
|
||||
@@ -28,7 +28,6 @@ from homeassistant.helpers.event import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_ENERGYID_KEY,
|
||||
CONF_HA_ENTITY_UUID,
|
||||
|
||||
@@ -15,13 +15,13 @@ from homeassistant.config_entries import (
|
||||
ConfigFlowResult,
|
||||
ConfigSubentryFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_DEVICE_ID
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
|
||||
|
||||
from .const import (
|
||||
CONF_DEVICE_ID,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_PROVISIONING_KEY,
|
||||
CONF_PROVISIONING_SECRET,
|
||||
|
||||
@@ -8,8 +8,6 @@ NAME: Final = "EnergyID"
|
||||
# --- Config Flow and Entry Data ---
|
||||
CONF_PROVISIONING_KEY: Final = "provisioning_key"
|
||||
CONF_PROVISIONING_SECRET: Final = "provisioning_secret"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_DEVICE_ID: Final = "device_id"
|
||||
CONF_DEVICE_NAME: Final = "device_name"
|
||||
|
||||
# --- Subentry (Mapping) Data ---
|
||||
|
||||
@@ -96,7 +96,7 @@ def __get_coordinator(call: ServiceCall) -> EnergyZeroDataUpdateCoordinator:
|
||||
"config_entry": entry_id,
|
||||
},
|
||||
)
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unloaded_config_entry",
|
||||
@@ -125,7 +125,7 @@ async def __get_prices(
|
||||
|
||||
data: Electricity | Gas
|
||||
|
||||
if price_type == PriceType.GAS:
|
||||
if price_type is PriceType.GAS:
|
||||
data = await coordinator.energyzero.get_gas_prices_legacy(
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
|
||||
@@ -4,10 +4,10 @@ from functools import partial
|
||||
|
||||
from aioesphomeapi import (
|
||||
AlarmControlPanelCommand,
|
||||
AlarmControlPanelEntityFeature as ESPHomeAlarmControlPanelEntityFeature,
|
||||
AlarmControlPanelEntityState as ESPHomeAlarmControlPanelEntityState,
|
||||
AlarmControlPanelInfo,
|
||||
AlarmControlPanelState as ESPHomeAlarmControlPanelState,
|
||||
APIIntEnum,
|
||||
EntityInfo,
|
||||
)
|
||||
|
||||
@@ -50,16 +50,28 @@ _ESPHOME_ACP_STATE_TO_HASS_STATE: EsphomeEnumMapper[
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class EspHomeACPFeatures(APIIntEnum):
|
||||
"""ESPHome AlarmControlPanel feature numbers."""
|
||||
|
||||
ARM_HOME = 1
|
||||
ARM_AWAY = 2
|
||||
ARM_NIGHT = 4
|
||||
TRIGGER = 8
|
||||
ARM_CUSTOM_BYPASS = 16
|
||||
ARM_VACATION = 32
|
||||
_FEATURES: dict[
|
||||
ESPHomeAlarmControlPanelEntityFeature, AlarmControlPanelEntityFeature
|
||||
] = {
|
||||
ESPHomeAlarmControlPanelEntityFeature.ARM_HOME: (
|
||||
AlarmControlPanelEntityFeature.ARM_HOME
|
||||
),
|
||||
ESPHomeAlarmControlPanelEntityFeature.ARM_AWAY: (
|
||||
AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
),
|
||||
ESPHomeAlarmControlPanelEntityFeature.ARM_NIGHT: (
|
||||
AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
),
|
||||
ESPHomeAlarmControlPanelEntityFeature.TRIGGER: (
|
||||
AlarmControlPanelEntityFeature.TRIGGER
|
||||
),
|
||||
ESPHomeAlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS: (
|
||||
AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
||||
),
|
||||
ESPHomeAlarmControlPanelEntityFeature.ARM_VACATION: (
|
||||
AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class EsphomeAlarmControlPanel(
|
||||
@@ -73,20 +85,14 @@ class EsphomeAlarmControlPanel(
|
||||
"""Set attrs from static info."""
|
||||
super()._on_static_info_update(static_info)
|
||||
static_info = self._static_info
|
||||
feature = 0
|
||||
if static_info.supported_features & EspHomeACPFeatures.ARM_HOME:
|
||||
feature |= AlarmControlPanelEntityFeature.ARM_HOME
|
||||
if static_info.supported_features & EspHomeACPFeatures.ARM_AWAY:
|
||||
feature |= AlarmControlPanelEntityFeature.ARM_AWAY
|
||||
if static_info.supported_features & EspHomeACPFeatures.ARM_NIGHT:
|
||||
feature |= AlarmControlPanelEntityFeature.ARM_NIGHT
|
||||
if static_info.supported_features & EspHomeACPFeatures.TRIGGER:
|
||||
feature |= AlarmControlPanelEntityFeature.TRIGGER
|
||||
if static_info.supported_features & EspHomeACPFeatures.ARM_CUSTOM_BYPASS:
|
||||
feature |= AlarmControlPanelEntityFeature.ARM_CUSTOM_BYPASS
|
||||
if static_info.supported_features & EspHomeACPFeatures.ARM_VACATION:
|
||||
feature |= AlarmControlPanelEntityFeature.ARM_VACATION
|
||||
self._attr_supported_features = AlarmControlPanelEntityFeature(feature)
|
||||
esp_flags = ESPHomeAlarmControlPanelEntityFeature(
|
||||
static_info.supported_features
|
||||
)
|
||||
flags = AlarmControlPanelEntityFeature(0)
|
||||
for esp_flag in esp_flags:
|
||||
if (flag := _FEATURES.get(esp_flag)) is not None:
|
||||
flags |= flag
|
||||
self._attr_supported_features = flags
|
||||
self._attr_code_format = (
|
||||
CodeFormat.NUMBER if static_info.requires_code else None
|
||||
)
|
||||
|
||||
@@ -1,16 +1,33 @@
|
||||
"""Bluetooth support for esphome."""
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from aioesphomeapi import APIClient, DeviceInfo
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIVersion,
|
||||
BluetoothProxyFeature,
|
||||
BluetoothScannerMode,
|
||||
BluetoothScannerStateResponse,
|
||||
DeviceInfo,
|
||||
)
|
||||
from bleak_esphome import connect_scanner
|
||||
|
||||
from homeassistant.components.bluetooth import async_register_scanner
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothScanningMode,
|
||||
async_register_scanner,
|
||||
)
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entry_data import RuntimeEntryData
|
||||
from .const import CONF_BLUETOOTH_SCANNING_MODE, DEFAULT_BLUETOOTH_SCANNING_MODE, DOMAIN
|
||||
from .entry_data import ESPHomeConfigEntry, RuntimeEntryData
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bleak_esphome.backend.scanner import ESPHomeScanner
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_VALID_SCANNING_MODES = {mode.value for mode in BluetoothScanningMode}
|
||||
|
||||
|
||||
@hass_callback
|
||||
@@ -23,6 +40,7 @@ def _async_unload(unload_callbacks: list[CALLBACK_TYPE]) -> None:
|
||||
@hass_callback
|
||||
def async_connect_scanner(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
entry_data: RuntimeEntryData,
|
||||
cli: APIClient,
|
||||
device_info: DeviceInfo,
|
||||
@@ -35,17 +53,75 @@ def async_connect_scanner(
|
||||
scanner = client_data.scanner
|
||||
if TYPE_CHECKING:
|
||||
assert scanner is not None
|
||||
return partial(
|
||||
_async_unload,
|
||||
[
|
||||
async_register_scanner(
|
||||
hass,
|
||||
scanner,
|
||||
source_domain=DOMAIN,
|
||||
source_model=device_info.model,
|
||||
source_config_entry_id=entry_data.entry_id,
|
||||
source_device_id=device_id,
|
||||
),
|
||||
scanner.async_setup(),
|
||||
],
|
||||
)
|
||||
api_version = cli.api_version or APIVersion()
|
||||
feature_flags = device_info.bluetooth_proxy_feature_flags_compat(api_version)
|
||||
state_and_mode = bool(feature_flags & BluetoothProxyFeature.FEATURE_STATE_AND_MODE)
|
||||
# Pin mode before async_register_scanner so habluetooth spawns the AUTO worker.
|
||||
deferred_migration: CALLBACK_TYPE | None = None
|
||||
if state_and_mode:
|
||||
deferred_migration = _async_apply_scanning_mode(hass, entry, scanner, cli)
|
||||
callbacks: list[CALLBACK_TYPE] = [
|
||||
async_register_scanner(
|
||||
hass,
|
||||
scanner,
|
||||
source_domain=DOMAIN,
|
||||
source_model=device_info.model,
|
||||
source_config_entry_id=entry_data.entry_id,
|
||||
source_device_id=device_id,
|
||||
),
|
||||
scanner.async_setup(),
|
||||
]
|
||||
if deferred_migration is not None:
|
||||
callbacks.append(deferred_migration)
|
||||
return partial(_async_unload, callbacks)
|
||||
|
||||
|
||||
@hass_callback
|
||||
def _async_apply_scanning_mode(
|
||||
hass: HomeAssistant,
|
||||
entry: ESPHomeConfigEntry,
|
||||
scanner: ESPHomeScanner,
|
||||
cli: APIClient,
|
||||
) -> CALLBACK_TYPE | None:
|
||||
"""Apply saved scanning mode synchronously; migrate from configured_mode later."""
|
||||
saved = entry.options.get(CONF_BLUETOOTH_SCANNING_MODE)
|
||||
if saved is not None and saved not in _VALID_SCANNING_MODES:
|
||||
_LOGGER.warning("%s: unknown scanning mode %r", entry.title, saved)
|
||||
saved = None
|
||||
initial_value = saved if saved is not None else DEFAULT_BLUETOOTH_SCANNING_MODE
|
||||
scanner.async_set_scanning_mode(BluetoothScanningMode(initial_value))
|
||||
if saved is not None:
|
||||
return None
|
||||
|
||||
unsub_holder: list[CALLBACK_TYPE] = []
|
||||
|
||||
@hass_callback
|
||||
def _migrate(state: BluetoothScannerStateResponse) -> None:
|
||||
# proto3 unset enums decode to None; wait for a real value.
|
||||
if (configured_pb := state.configured_mode) is None:
|
||||
return
|
||||
if unsub_holder:
|
||||
unsub_holder.pop()()
|
||||
if configured_pb is BluetoothScannerMode.PASSIVE:
|
||||
new_mode = BluetoothScanningMode.PASSIVE
|
||||
else:
|
||||
new_mode = BluetoothScanningMode(DEFAULT_BLUETOOTH_SCANNING_MODE)
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
options={
|
||||
**entry.options,
|
||||
CONF_BLUETOOTH_SCANNING_MODE: new_mode.value,
|
||||
},
|
||||
)
|
||||
# AUTO -> AUTO is already pinned; only re-apply on a downgrade.
|
||||
if new_mode is not BluetoothScanningMode(DEFAULT_BLUETOOTH_SCANNING_MODE):
|
||||
scanner.async_set_scanning_mode(new_mode)
|
||||
|
||||
unsub_holder.append(cli.subscribe_bluetooth_scanner_state(_migrate))
|
||||
|
||||
@hass_callback
|
||||
def _unsubscribe() -> None:
|
||||
if unsub_holder:
|
||||
unsub_holder.pop()()
|
||||
|
||||
return _unsubscribe
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any, cast
|
||||
from aioesphomeapi import (
|
||||
APIClient,
|
||||
APIConnectionError,
|
||||
BluetoothProxyFeature,
|
||||
DeviceInfo,
|
||||
InvalidAuthAPIError,
|
||||
InvalidEncryptionKeyAPIError,
|
||||
@@ -20,6 +21,7 @@ import aiohttp
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_ESPHOME,
|
||||
SOURCE_IGNORE,
|
||||
@@ -38,6 +40,11 @@ from homeassistant.data_entry_flow import AbortFlow, FlowResultType
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.importlib import async_import_module
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.esphome import ESPHomeServiceInfo
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
@@ -47,10 +54,12 @@ from homeassistant.util.json import json_loads_object
|
||||
|
||||
from .const import (
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
CONF_BLUETOOTH_SCANNING_MODE,
|
||||
CONF_DEVICE_NAME,
|
||||
CONF_NOISE_PSK,
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
DEFAULT_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_BLUETOOTH_SCANNING_MODE,
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS,
|
||||
DEFAULT_PORT,
|
||||
DOMAIN,
|
||||
@@ -68,6 +77,18 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ZERO_NOISE_PSK = "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA="
|
||||
DEFAULT_NAME = "ESPHome"
|
||||
|
||||
_BLUETOOTH_SCANNING_MODE_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[
|
||||
BluetoothScanningMode.AUTO.value,
|
||||
BluetoothScanningMode.ACTIVE.value,
|
||||
BluetoothScanningMode.PASSIVE.value,
|
||||
],
|
||||
translation_key="bluetooth_scanning_mode",
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a esphome config flow."""
|
||||
@@ -936,18 +957,44 @@ class OptionsFlowHandler(OptionsFlowWithReload):
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
options = self.config_entry.options
|
||||
schema: dict[Any, Any] = {
|
||||
vol.Required(
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
default=options.get(
|
||||
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||
),
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
default=options.get(CONF_SUBSCRIBE_LOGS, False),
|
||||
): bool,
|
||||
}
|
||||
if _entry_has_bluetooth_scanner(self.config_entry):
|
||||
schema[
|
||||
vol.Required(
|
||||
CONF_ALLOW_SERVICE_CALLS,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_ALLOW_SERVICE_CALLS, DEFAULT_ALLOW_SERVICE_CALLS
|
||||
CONF_BLUETOOTH_SCANNING_MODE,
|
||||
default=options.get(
|
||||
CONF_BLUETOOTH_SCANNING_MODE, DEFAULT_BLUETOOTH_SCANNING_MODE
|
||||
),
|
||||
): bool,
|
||||
vol.Required(
|
||||
CONF_SUBSCRIBE_LOGS,
|
||||
default=self.config_entry.options.get(CONF_SUBSCRIBE_LOGS, False),
|
||||
): bool,
|
||||
}
|
||||
)
|
||||
] = _BLUETOOTH_SCANNING_MODE_SELECTOR
|
||||
return self.async_show_form(step_id="init", data_schema=vol.Schema(schema))
|
||||
|
||||
|
||||
@callback
|
||||
def _entry_has_bluetooth_scanner(entry: ESPHomeConfigEntry) -> bool:
|
||||
"""Return True if the entry exposes a bluetooth proxy scanner or has one saved."""
|
||||
# Keep showing the option if it was previously saved, even when the
|
||||
# device is offline or stops advertising the feature flag, so the
|
||||
# saved value isn't silently dropped on the next options save.
|
||||
if CONF_BLUETOOTH_SCANNING_MODE in entry.options:
|
||||
return True
|
||||
if entry.state is ConfigEntryState.LOADED and (
|
||||
device_info := entry.runtime_data.device_info
|
||||
):
|
||||
flags = device_info.bluetooth_proxy_feature_flags_compat(
|
||||
entry.runtime_data.api_version
|
||||
)
|
||||
return self.async_show_form(step_id="init", data_schema=data_schema)
|
||||
return bool(flags & BluetoothProxyFeature.FEATURE_STATE_AND_MODE)
|
||||
return False
|
||||
|
||||
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Final
|
||||
|
||||
from awesomeversion import AwesomeVersion
|
||||
|
||||
from homeassistant.components.bluetooth import BluetoothScanningMode
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -18,9 +19,11 @@ CONF_SUBSCRIBE_LOGS = "subscribe_logs"
|
||||
CONF_DEVICE_NAME = "device_name"
|
||||
CONF_NOISE_PSK = "noise_psk"
|
||||
CONF_BLUETOOTH_MAC_ADDRESS = "bluetooth_mac_address"
|
||||
CONF_BLUETOOTH_SCANNING_MODE = "bluetooth_scanning_mode"
|
||||
|
||||
DEFAULT_ALLOW_SERVICE_CALLS = True
|
||||
DEFAULT_NEW_CONFIG_ALLOW_ALLOW_SERVICE_CALLS = False
|
||||
DEFAULT_BLUETOOTH_SCANNING_MODE = BluetoothScanningMode.AUTO.value
|
||||
|
||||
DEFAULT_PORT: Final = 6053
|
||||
|
||||
|
||||
@@ -669,7 +669,7 @@ class ESPHomeManager:
|
||||
if device_info.bluetooth_proxy_feature_flags_compat(api_version):
|
||||
entry_data.disconnect_callbacks.add(
|
||||
async_connect_scanner(
|
||||
hass, entry_data, cli, device_info, self.device_id
|
||||
hass, self.entry, entry_data, cli, device_info, self.device_id
|
||||
)
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==45.0.4",
|
||||
"aioesphomeapi==45.2.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.7.3"
|
||||
"bleak-esphome==3.9.1"
|
||||
],
|
||||
"zeroconf": ["_esphomelib._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -209,13 +209,24 @@
|
||||
"init": {
|
||||
"data": {
|
||||
"allow_service_calls": "Allow the device to perform Home Assistant actions.",
|
||||
"bluetooth_scanning_mode": "Bluetooth scanning mode",
|
||||
"subscribe_logs": "Subscribe to logs from the device."
|
||||
},
|
||||
"data_description": {
|
||||
"allow_service_calls": "When enabled, ESPHome devices can perform Home Assistant actions or send events. Only enable this if you trust the device.",
|
||||
"bluetooth_scanning_mode": "Auto is recommended for most setups. It saves battery on your Bluetooth devices while still catching new devices and updates quickly.",
|
||||
"subscribe_logs": "When enabled, the device will send logs to Home Assistant and you can view them in the logs panel."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"bluetooth_scanning_mode": {
|
||||
"options": {
|
||||
"active": "Active (uses more device battery, fastest updates)",
|
||||
"auto": "Auto (recommended, saves device battery)",
|
||||
"passive": "Passive (lowest device battery use, some details may be missing)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"service": "mdi:refresh"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"service": "mdi:water-heater"
|
||||
"service": "mdi:water-boiler"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"service": "mdi:pencil"
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
if entry.state == ConfigEntryState.LOADED:
|
||||
if entry.state is ConfigEntryState.LOADED:
|
||||
await coordinator.async_refresh()
|
||||
else:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -16,7 +16,7 @@ class DeviceType(Enum):
|
||||
GAME_CONSOLE = "mdi:nintendo-game-boy"
|
||||
STREAMING_DONGLE = "mdi:cast"
|
||||
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
|
||||
DISC_PLAYER = "mdi:disk-player"
|
||||
DISC_PLAYER = "mdi:disc-player"
|
||||
REMOTE_CONTROL = "mdi:remote-tv"
|
||||
RADIO = "mdi:radio"
|
||||
PHOTO_CAMERA = PHOTOS = "mdi:camera"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyfireservicerota import (
|
||||
ExpiredTokenError,
|
||||
@@ -177,10 +178,12 @@ class FireServiceRotaClient:
|
||||
|
||||
if await self.oauth.async_refresh_tokens():
|
||||
self.token_refresh_failure = False
|
||||
await self._hass.async_add_executor_job(self.websocket.start_listener)
|
||||
|
||||
# pylint: disable-next=home-assistant-sequential-executor-jobs
|
||||
return await self._hass.async_add_executor_job(func, *args)
|
||||
def _restart_and_call() -> Any:
|
||||
self.websocket.start_listener()
|
||||
return func(*args)
|
||||
|
||||
return await self._hass.async_add_executor_job(_restart_and_call)
|
||||
|
||||
async def async_update(self) -> dict | None:
|
||||
"""Get the latest availability data."""
|
||||
|
||||
@@ -5,11 +5,10 @@ import logging
|
||||
from fishaudio import AsyncFishAudio
|
||||
from fishaudio.exceptions import AuthenticationError, FishAudioError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
|
||||
from .const import CONF_API_KEY
|
||||
from .types import FishAudioConfigEntry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -16,6 +16,7 @@ from homeassistant.config_entries import (
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LANGUAGE, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.selector import (
|
||||
LanguageSelector,
|
||||
@@ -29,11 +30,8 @@ from homeassistant.helpers.selector import (
|
||||
from .const import (
|
||||
API_KEYS_URL,
|
||||
BACKEND_MODELS,
|
||||
CONF_API_KEY,
|
||||
CONF_BACKEND,
|
||||
CONF_LANGUAGE,
|
||||
CONF_LATENCY,
|
||||
CONF_NAME,
|
||||
CONF_SELF_ONLY,
|
||||
CONF_SORT_BY,
|
||||
CONF_TITLE,
|
||||
@@ -284,7 +282,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage initial options."""
|
||||
entry = self._get_entry()
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
self.client = entry.runtime_data
|
||||
|
||||
@@ -4,17 +4,10 @@ from typing import Literal
|
||||
|
||||
DOMAIN = "fish_audio"
|
||||
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_NAME: Literal["name"] = "name"
|
||||
CONF_USER_ID: Literal["user_id"] = "user_id"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_API_KEY: Literal["api_key"] = "api_key"
|
||||
CONF_VOICE_ID: Literal["voice_id"] = "voice_id"
|
||||
CONF_BACKEND: Literal["backend"] = "backend"
|
||||
CONF_SELF_ONLY: Literal["self_only"] = "self_only"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
CONF_LANGUAGE: Literal["language"] = "language"
|
||||
CONF_SORT_BY: Literal["sort_by"] = "sort_by"
|
||||
CONF_LATENCY: Literal["latency"] = "latency"
|
||||
CONF_TITLE: Literal["title"] = "title"
|
||||
|
||||
@@ -13,8 +13,6 @@ ATTR_LAST_SAVED_AT: Final = "last_saved_at"
|
||||
|
||||
ATTR_DURATION: Final = "duration"
|
||||
ATTR_DISTANCE: Final = "distance"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_ELEVATION: Final = "elevation"
|
||||
ATTR_HEIGHT: Final = "height"
|
||||
ATTR_WEIGHT: Final = "weight"
|
||||
ATTR_BODY: Final = "body"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user