mirror of
https://github.com/home-assistant/core.git
synced 2026-05-27 03:05:50 +02:00
Compare commits
164 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6dea67e8c9 | |||
| 92ce5ed75a | |||
| 466e28eae2 | |||
| 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 | |||
| a02e54f332 | |||
| 1858649bc7 | |||
| 109e09c3ec | |||
| ad139b259b | |||
| a1a76874fd | |||
| e7bd56325b | |||
| ef2ef0c8ba | |||
| a8381e923a | |||
| b7adba559b | |||
| 5cf6dceb04 | |||
| 975bcc5431 | |||
| f24a44e81f | |||
| 43c91843cd | |||
| dbce1d328a | |||
| d294b04b79 | |||
| 8b0e9060b3 | |||
| 39066b6e3a | |||
| a23a9b350b | |||
| fdaa807ca8 | |||
| f290dcc03f |
@@ -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
+4
-6
@@ -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
|
||||
@@ -1413,8 +1411,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pushover/ @engrbm87
|
||||
/homeassistant/components/pvoutput/ @frenck
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/homeassistant/components/pyload/ @tr4nt0r
|
||||
/tests/components/pyload/ @tr4nt0r
|
||||
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError
|
||||
@@ -204,17 +204,20 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
||||
self._attr_unique_id = unique_id
|
||||
self._supports_offset = supports_offset
|
||||
|
||||
@override
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
return self._event
|
||||
|
||||
@override
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
return await self.coordinator.async_get_events(hass, start_date, end_date)
|
||||
|
||||
@override
|
||||
async def async_create_event(self, **kwargs: Any) -> None:
|
||||
"""Create a new event in the calendar."""
|
||||
_LOGGER.debug("Event: %s", kwargs)
|
||||
@@ -240,6 +243,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Update event data."""
|
||||
@@ -255,6 +259,7 @@ class WebDavCalendarEntity(CoordinatorEntity[CalDavUpdateCoordinator], CalendarE
|
||||
}
|
||||
super()._handle_coordinator_update()
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to hass update state from existing coordinator data."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import AuthorizationError, DAVError
|
||||
@@ -33,6 +33,7 @@ class CalDavConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import date, datetime, time, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import caldav
|
||||
|
||||
@@ -90,6 +90,7 @@ class CalDavUpdateCoordinator(DataUpdateCoordinator[CalendarEvent | None]):
|
||||
|
||||
return event_list
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> CalendarEvent | None:
|
||||
"""Get the latest data."""
|
||||
start_of_today = dt_util.start_of_local_day()
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
from datetime import date, datetime, timedelta
|
||||
from functools import partial
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
from typing import Any, cast, override
|
||||
|
||||
import caldav
|
||||
from caldav.lib.error import DAVError, NotFoundError
|
||||
@@ -121,6 +121,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
if (todo_item := _todo_item(resource)) is not None
|
||||
]
|
||||
|
||||
@override
|
||||
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||
"""Add an item to the To-do list."""
|
||||
item_data: dict[str, Any] = {}
|
||||
@@ -141,6 +142,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
@override
|
||||
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||
"""Update a To-do item."""
|
||||
uid: str = cast(str, item.uid)
|
||||
@@ -177,6 +179,7 @@ class WebDavTodoListEntity(TodoListEntity):
|
||||
except (requests.ConnectionError, requests.Timeout, DAVError) as err:
|
||||
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||
|
||||
@override
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
"""Delete To-do items."""
|
||||
tasks = (
|
||||
|
||||
@@ -7,7 +7,7 @@ from http import HTTPStatus
|
||||
from itertools import groupby
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, Final, cast, final
|
||||
from typing import Any, Final, cast, final, override
|
||||
|
||||
from aiohttp import web
|
||||
from dateutil.rrule import rrulestr
|
||||
@@ -546,6 +546,7 @@ class CalendarEntity(Entity):
|
||||
return self.entity_description.initial_color
|
||||
return None
|
||||
|
||||
@override
|
||||
def get_initial_entity_options(self) -> er.EntityOptionsType | None:
|
||||
"""Return initial entity options."""
|
||||
if self.initial_color is None:
|
||||
@@ -564,6 +565,7 @@ class CalendarEntity(Entity):
|
||||
"""Return the next upcoming event."""
|
||||
raise NotImplementedError
|
||||
|
||||
@override
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, Any] | None:
|
||||
@@ -580,6 +582,7 @@ class CalendarEntity(Entity):
|
||||
"description": event.description or "",
|
||||
}
|
||||
|
||||
@override
|
||||
@final
|
||||
@property
|
||||
def state(self) -> str:
|
||||
@@ -594,6 +597,7 @@ class CalendarEntity(Entity):
|
||||
|
||||
return STATE_OFF
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine.
|
||||
@@ -653,6 +657,7 @@ class CalendarEntity(Entity):
|
||||
self._event_listener_debouncer.async_cancel()
|
||||
self._event_listener_debouncer = None
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Run when entity will be removed from hass.
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
import datetime
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from typing import TYPE_CHECKING, Any, cast, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -113,6 +113,7 @@ class Timespan:
|
||||
"""
|
||||
return Timespan(self.end, max(self.end, now) + interval)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
"""Return a string representing the half open interval time span."""
|
||||
return f"[{self.start}, {self.end})"
|
||||
@@ -325,6 +326,7 @@ class TargetCalendarEventListener(TargetEntityChangeTracker):
|
||||
self._pending_listener_task: asyncio.Task[None] | None = None
|
||||
self._calendar_event_listener: CalendarEventListener | None = None
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _handle_entities_update(self, tracked_entities: set[str]) -> None:
|
||||
"""Restart listeners when tracked target entities update."""
|
||||
@@ -351,6 +353,7 @@ class TargetCalendarEventListener(TargetEntityChangeTracker):
|
||||
)
|
||||
await self._calendar_event_listener.async_attach()
|
||||
|
||||
@override
|
||||
def _unsubscribe(self) -> None:
|
||||
"""Unsubscribe from all events."""
|
||||
super()._unsubscribe()
|
||||
@@ -367,6 +370,7 @@ class SingleEntityEventTrigger(Trigger):
|
||||
|
||||
_options: dict[str, Any]
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_complete_config(
|
||||
cls, hass: HomeAssistant, complete_config: ConfigType
|
||||
@@ -377,6 +381,7 @@ class SingleEntityEventTrigger(Trigger):
|
||||
)
|
||||
return await super().async_validate_complete_config(hass, complete_config)
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
@@ -392,6 +397,7 @@ class SingleEntityEventTrigger(Trigger):
|
||||
assert config.options is not None
|
||||
self._options = config.options
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
@@ -426,6 +432,7 @@ class EventTrigger(Trigger):
|
||||
_options: dict[str, Any]
|
||||
_event_type: str
|
||||
|
||||
@override
|
||||
@classmethod
|
||||
async def async_validate_config(
|
||||
cls, hass: HomeAssistant, config: ConfigType
|
||||
@@ -443,6 +450,7 @@ class EventTrigger(Trigger):
|
||||
self._target = config.target
|
||||
self._options = config.options
|
||||
|
||||
@override
|
||||
async def async_attach_runner(
|
||||
self, run_action: TriggerActionRunner
|
||||
) -> CALLBACK_TYPE:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for Cambridge Audio."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
import voluptuous as vol
|
||||
@@ -29,6 +29,7 @@ class CambridgeAudioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Initialize the config flow."""
|
||||
self.data: dict[str, Any] = {}
|
||||
|
||||
@override
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
@@ -81,6 +82,7 @@ class CambridgeAudioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Concatenate
|
||||
from typing import Any, Concatenate, override
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
from aiostreammagic.models import CallbackType
|
||||
@@ -60,15 +60,18 @@ class CambridgeAudioEntity(Entity):
|
||||
"""Call when the device is notified of changes."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.client.is_connected()
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register callback handlers."""
|
||||
await self.client.register_state_update_callbacks(self._state_update_callback)
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Remove callbacks."""
|
||||
self.client.unregister_state_update_callbacks(self._state_update_callback)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Support for Cambridge Audio AV Receiver."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from aiostreammagic import (
|
||||
RepeatMode as CambridgeRepeatMode,
|
||||
@@ -83,6 +83,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
super().__init__(client)
|
||||
self._attr_unique_id = client.info.unit_id
|
||||
|
||||
@override
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Supported features for the media player."""
|
||||
@@ -100,6 +101,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
features |= feature
|
||||
return features
|
||||
|
||||
@override
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Return the state of the device."""
|
||||
@@ -118,11 +120,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
return MediaPlayerState.ON
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@override
|
||||
@property
|
||||
def source_list(self) -> list[str]:
|
||||
"""Return a list of available input sources."""
|
||||
return [item.name for item in self.client.sources]
|
||||
|
||||
@override
|
||||
@property
|
||||
def source(self) -> str | None:
|
||||
"""Return the current input source."""
|
||||
@@ -135,11 +139,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
None,
|
||||
)
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_title(self) -> str | None:
|
||||
"""Title of current playing media."""
|
||||
return self.client.play_state.metadata.title
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_artist(self) -> str | None:
|
||||
"""Artist of current playing media, music track only."""
|
||||
@@ -151,52 +157,62 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
return self.client.play_state.metadata.station
|
||||
return self.client.play_state.metadata.artist
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_album_name(self) -> str | None:
|
||||
"""Album name of current playing media, music track only."""
|
||||
return self.client.play_state.metadata.album
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_image_url(self) -> str | None:
|
||||
"""Image url of current playing media."""
|
||||
return self.client.play_state.metadata.art_url
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_duration(self) -> int | None:
|
||||
"""Duration of the current media."""
|
||||
return self.client.play_state.metadata.duration
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_position(self) -> int | None:
|
||||
"""Position of the current media."""
|
||||
return self.client.play_state.position
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_position_updated_at(self) -> datetime:
|
||||
"""Last time the media position was updated."""
|
||||
return self.client.position_last_updated
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
return self.client.play_state.metadata.station
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_volume_muted(self) -> bool | None:
|
||||
"""Volume mute status."""
|
||||
return self.client.state.mute
|
||||
|
||||
@override
|
||||
@property
|
||||
def volume_level(self) -> float | None:
|
||||
"""Current pre-amp volume level."""
|
||||
volume = self.client.state.volume_percent or 0
|
||||
return volume / 100
|
||||
|
||||
@override
|
||||
@property
|
||||
def shuffle(self) -> bool:
|
||||
"""Current shuffle configuration."""
|
||||
return self.client.play_state.mode_shuffle != ShuffleMode.OFF
|
||||
|
||||
@override
|
||||
@property
|
||||
def repeat(self) -> RepeatMode | None:
|
||||
"""Current repeat configuration."""
|
||||
@@ -205,11 +221,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
mode_repeat = RepeatMode.ALL
|
||||
return mode_repeat
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Toggle play/pause the current media."""
|
||||
await self.client.play_pause()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_pause(self) -> None:
|
||||
"""Pause the current media."""
|
||||
@@ -222,11 +240,13 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
else:
|
||||
await self.client.pause()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_stop(self) -> None:
|
||||
"""Stop the current media."""
|
||||
await self.client.stop()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_play(self) -> None:
|
||||
"""Play the current media."""
|
||||
@@ -239,16 +259,19 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
else:
|
||||
await self.client.play()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_next_track(self) -> None:
|
||||
"""Skip to the next track."""
|
||||
await self.client.next_track()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_previous_track(self) -> None:
|
||||
"""Skip to the previous track."""
|
||||
await self.client.previous_track()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select the source."""
|
||||
@@ -257,41 +280,49 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
await self.client.set_source_by_id(src.id)
|
||||
break
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Power on the device."""
|
||||
await self.client.power_on()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Power off the device."""
|
||||
await self.client.power_off()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Step the volume up."""
|
||||
await self.client.volume_up()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Step the volume down."""
|
||||
await self.client.volume_down()
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set the volume level."""
|
||||
await self.client.set_volume(int(volume * 100))
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Set the mute state."""
|
||||
await self.client.set_mute(mute)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_media_seek(self, position: float) -> None:
|
||||
"""Seek to a position in the current media."""
|
||||
await self.client.media_seek(int(position))
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_set_shuffle(self, shuffle: bool) -> None:
|
||||
"""Set the shuffle mode for the current queue."""
|
||||
@@ -300,6 +331,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
shuffle_mode = ShuffleMode.ALL
|
||||
await self.client.set_shuffle(shuffle_mode)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_set_repeat(self, repeat: RepeatMode) -> None:
|
||||
"""Set the repeat mode for the current queue."""
|
||||
@@ -308,6 +340,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
repeat_mode = CambridgeRepeatMode.ALL
|
||||
await self.client.set_repeat(repeat_mode)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@@ -353,6 +386,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
||||
if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
|
||||
await self.client.play_radio_url("Radio", media_id)
|
||||
|
||||
@override
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
|
||||
@@ -90,16 +90,19 @@ class CambridgeAudioNumber(CambridgeAudioEntity, NumberEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state of the number."""
|
||||
return self.entity_description.value_fn(self.client)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Set the selected value."""
|
||||
await self.entity_description.set_value_fn(self.client, int(value))
|
||||
|
||||
@override
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import override
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
from aiostreammagic.models import ControlBusMode, DisplayBrightness
|
||||
@@ -127,11 +128,13 @@ class CambridgeAudioSelect(CambridgeAudioEntity, SelectEntity):
|
||||
if options_fn:
|
||||
self._attr_options = options_fn
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the state of the select."""
|
||||
return self.entity_description.value_fn(self.client)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from aiostreammagic import StreamMagicClient
|
||||
|
||||
@@ -103,16 +103,19 @@ class CambridgeAudioSwitch(CambridgeAudioEntity, SwitchEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{client.info.unit_id}-{description.key}"
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the state of the switch."""
|
||||
return self.entity_description.value_fn(self.client)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
await self.entity_description.set_value_fn(self.client, True)
|
||||
|
||||
@override
|
||||
@command
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
|
||||
@@ -12,7 +12,7 @@ import logging
|
||||
import os
|
||||
from random import SystemRandom
|
||||
import time
|
||||
from typing import Any, Final, final
|
||||
from typing import Any, Final, final, override
|
||||
|
||||
from aiohttp import hdrs, web
|
||||
import attr
|
||||
@@ -455,6 +455,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
!= Camera.async_handle_async_webrtc_offer
|
||||
)
|
||||
|
||||
@override
|
||||
@cached_property
|
||||
def entity_picture(self) -> str:
|
||||
"""Return a link to the camera feed as entity picture."""
|
||||
@@ -467,6 +468,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Whether or not to use stream to generate stills."""
|
||||
return False
|
||||
|
||||
@override
|
||||
@cached_property
|
||||
def supported_features(self) -> CameraEntityFeature:
|
||||
"""Flag supported features."""
|
||||
@@ -502,6 +504,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Return the interval between frames of the mjpeg stream."""
|
||||
return self._attr_frame_interval
|
||||
|
||||
@override
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
@@ -595,6 +598,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""
|
||||
return await self.handle_async_still_stream(request, self.frame_interval)
|
||||
|
||||
@override
|
||||
@property
|
||||
@final
|
||||
def state(self) -> str:
|
||||
@@ -642,6 +646,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
"""Call the job and disable motion detection."""
|
||||
await self.hass.async_add_executor_job(self.disable_motion_detection)
|
||||
|
||||
@override
|
||||
@final
|
||||
@property
|
||||
def state_attributes(self) -> dict[str, str | None]:
|
||||
@@ -665,6 +670,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
self.access_tokens.append(hex(_RND.getrandbits(256))[2:])
|
||||
self.__dict__.pop("entity_picture", None)
|
||||
|
||||
@override
|
||||
async def async_internal_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
await super().async_internal_added_to_hass()
|
||||
@@ -758,6 +764,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
|
||||
|
||||
return CameraCapabilities(frontend_stream_types)
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _async_write_ha_state(self) -> None:
|
||||
"""Write the state to the state machine.
|
||||
@@ -817,6 +824,7 @@ class CameraImageView(CameraView):
|
||||
url = "/api/camera_proxy/{entity_id}"
|
||||
name = "api:camera:image"
|
||||
|
||||
@override
|
||||
async def handle(self, request: web.Request, camera: Camera) -> web.Response:
|
||||
"""Serve camera image."""
|
||||
width = request.query.get("width")
|
||||
@@ -840,6 +848,7 @@ class CameraMjpegStream(CameraView):
|
||||
url = "/api/camera_proxy_stream/{entity_id}"
|
||||
name = "api:camera:stream"
|
||||
|
||||
@override
|
||||
async def handle(self, request: web.Request, camera: Camera) -> web.StreamResponse:
|
||||
"""Serve camera stream, possibly with interval."""
|
||||
if (interval_str := request.query.get("interval")) is None:
|
||||
@@ -985,6 +994,7 @@ class _TemplateCameraEntity:
|
||||
self._report_issue()
|
||||
return getattr(self._camera, name)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
"""Forward to the camera entity."""
|
||||
self._report_issue()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Expose cameras as media sources."""
|
||||
|
||||
import asyncio
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.media_player import BrowseError, MediaClass
|
||||
from homeassistant.components.media_source import (
|
||||
@@ -54,6 +55,7 @@ class CameraMediaSource(MediaSource):
|
||||
super().__init__(DOMAIN)
|
||||
self.hass = hass
|
||||
|
||||
@override
|
||||
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
|
||||
"""Resolve media to a url."""
|
||||
component = self.hass.data[DATA_COMPONENT]
|
||||
@@ -82,6 +84,7 @@ class CameraMediaSource(MediaSource):
|
||||
|
||||
return PlayMedia(url, FORMAT_CONTENT_TYPE[HLS_PROVIDER])
|
||||
|
||||
@override
|
||||
async def async_browse_media(
|
||||
self,
|
||||
item: MediaSourceItem,
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Awaitable, Callable
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from functools import cache, partial, wraps
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from mashumaro import MissingField
|
||||
import voluptuous as vol
|
||||
@@ -73,6 +73,7 @@ class WebRTCCandidate(WebRTCMessage):
|
||||
|
||||
candidate: RTCIceCandidate | RTCIceCandidateInit
|
||||
|
||||
@override
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
"""Return a dict representation of the message."""
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Support for Canary alarm."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from canary.const import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT
|
||||
from canary.model import Location
|
||||
@@ -58,6 +58,7 @@ class CanaryAlarm(
|
||||
"""Return information about the location."""
|
||||
return self.coordinator.data["locations"][self._location_id]
|
||||
|
||||
@override
|
||||
@property
|
||||
def alarm_state(self) -> AlarmControlPanelState | None:
|
||||
"""Return the state of the device."""
|
||||
@@ -74,25 +75,30 @@ class CanaryAlarm(
|
||||
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the state attributes."""
|
||||
return {"private": self.location.is_private}
|
||||
|
||||
@override
|
||||
def alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
self.coordinator.canary.set_location_mode(
|
||||
self._location_id, self.location.mode.name, True
|
||||
)
|
||||
|
||||
@override
|
||||
def alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_HOME)
|
||||
|
||||
@override
|
||||
def alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
self.coordinator.canary.set_location_mode(self._location_id, LOCATION_MODE_AWAY)
|
||||
|
||||
@override
|
||||
def alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
self.coordinator.canary.set_location_mode(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
from typing import Final, override
|
||||
|
||||
from aiohttp.web import Request, StreamResponse
|
||||
from canary.live_stream_api import LiveStreamSession
|
||||
@@ -108,16 +108,19 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera):
|
||||
"""Return information about the location."""
|
||||
return self.coordinator.data["locations"][self._location_id]
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_recording(self) -> bool:
|
||||
"""Return true if the device is recording."""
|
||||
return self.location.is_recording # type: ignore[no-any-return]
|
||||
|
||||
@override
|
||||
@property
|
||||
def motion_detection_enabled(self) -> bool:
|
||||
"""Return the camera motion detection status."""
|
||||
return not self.location.is_recording
|
||||
|
||||
@override
|
||||
async def async_camera_image(
|
||||
self, width: int | None = None, height: int | None = None
|
||||
) -> bytes | None:
|
||||
@@ -150,6 +153,7 @@ class CanaryCamera(CoordinatorEntity[CanaryDataUpdateCoordinator], Camera):
|
||||
|
||||
return self._image
|
||||
|
||||
@override
|
||||
async def handle_async_mjpeg_stream(
|
||||
self, request: Request
|
||||
) -> StreamResponse | None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for Canary."""
|
||||
|
||||
import logging
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, override
|
||||
|
||||
from canary.api import Api
|
||||
from requests.exceptions import ConnectTimeout, HTTPError
|
||||
@@ -46,12 +46,14 @@ class CanaryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return CanaryOptionsFlowHandler()
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -4,6 +4,7 @@ import asyncio
|
||||
from collections.abc import ValuesView
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from canary.api import Api
|
||||
from canary.model import Location, Reading
|
||||
@@ -60,6 +61,7 @@ class CanaryDataUpdateCoordinator(DataUpdateCoordinator[CanaryData]):
|
||||
"readings": readings_by_device_id,
|
||||
}
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> CanaryData:
|
||||
"""Fetch data from Canary."""
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Support for Canary sensors."""
|
||||
|
||||
from typing import Final
|
||||
from typing import Final, override
|
||||
|
||||
from canary.model import Device, Location, SensorType
|
||||
|
||||
@@ -143,11 +143,13 @@ class CanarySensor(CoordinatorEntity[CanaryDataUpdateCoordinator], SensorEntity)
|
||||
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.reading
|
||||
|
||||
@override
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||
"""Return the state attributes."""
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Casper Glow integration binary sensor platform."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
@@ -43,6 +45,7 @@ class CasperGlowPausedBinarySensor(CasperGlowEntity, BinarySensorEntity):
|
||||
if coordinator.device.state.is_paused is not None:
|
||||
self._attr_is_on = coordinator.device.state.is_paused
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -71,6 +74,7 @@ class CasperGlowChargingBinarySensor(CasperGlowEntity, BinarySensorEntity):
|
||||
if coordinator.device.state.is_charging is not None:
|
||||
self._attr_is_on = coordinator.device.state.is_charging
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
@@ -66,6 +67,7 @@ class CasperGlowButton(CasperGlowEntity, ButtonEntity):
|
||||
f"{format_mac(coordinator.device.address)}_{description.key}"
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_press(self) -> None:
|
||||
"""Press the button."""
|
||||
await self._async_command(self.entity_description.press_fn(self._device))
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for Casper Glow integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from bluetooth_data_tools import human_readable_name
|
||||
from pycasperglow import CasperGlow, CasperGlowError
|
||||
@@ -31,6 +31,7 @@ class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
|
||||
@override
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
@@ -73,6 +74,7 @@ class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders=self.context["title_placeholders"],
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Coordinator for the Casper Glow integration."""
|
||||
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from bleak import BleakError
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
@@ -74,6 +75,7 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
||||
"""Poll device state."""
|
||||
await self.device.query_state()
|
||||
|
||||
@override
|
||||
async def _async_poll(self) -> None:
|
||||
"""Poll the device and log availability changes."""
|
||||
assert self._last_service_info
|
||||
@@ -99,6 +101,7 @@ class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
||||
|
||||
self._async_handle_bluetooth_poll()
|
||||
|
||||
@override
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Casper Glow integration light platform."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
@@ -54,6 +54,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
self._attr_unique_id = format_mac(coordinator.device.address)
|
||||
self._update_from_state(coordinator.device.state)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -77,6 +78,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
self._update_from_state(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
brightness_pct: int | None = None
|
||||
@@ -98,6 +100,7 @@ class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
|
||||
self.coordinator.last_brightness_pct = brightness_pct
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self._async_command(self._device.turn_off())
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Casper Glow integration select platform for dimming time."""
|
||||
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
@@ -38,6 +40,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{format_mac(coordinator.device.address)}_dimming_time"
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_option(self) -> str | None:
|
||||
"""Return the currently selected dimming time from the coordinator."""
|
||||
@@ -45,6 +48,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
|
||||
return None
|
||||
return str(self.coordinator.last_dimming_time_minutes)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last known dimming time and register state update callback."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -75,6 +79,7 @@ class CasperGlowDimmingTimeSelect(CasperGlowEntity, SelectEntity, RestoreEntity)
|
||||
# to update the current state.
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Set the dimming time."""
|
||||
await self._async_command(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Casper Glow integration sensor platform."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import override
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
@@ -51,6 +52,7 @@ class CasperGlowBatterySensor(CasperGlowEntity, SensorEntity):
|
||||
if coordinator.device.state.battery_level is not None:
|
||||
self._attr_native_value = coordinator.device.state.battery_level.percentage
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
@@ -93,6 +95,7 @@ class CasperGlowDimmingEndTimeSensor(CasperGlowEntity, SensorEntity):
|
||||
"""Calculate projected dimming end time from remaining milliseconds."""
|
||||
return utcnow() + timedelta(milliseconds=remaining_ms)
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Config flow for Cast."""
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -47,6 +47,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(
|
||||
@@ -55,12 +56,14 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
"""Get the options flow for this handler."""
|
||||
return CastOptionsFlowHandler()
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
return await self.async_step_config()
|
||||
|
||||
@override
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: ZeroconfServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import logging
|
||||
import threading
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
import pychromecast.discovery
|
||||
import pychromecast.models
|
||||
@@ -67,14 +67,17 @@ def setup_internal_discovery(
|
||||
class CastListener(pychromecast.discovery.AbstractCastListener):
|
||||
"""Listener for discovering chromecasts."""
|
||||
|
||||
@override
|
||||
def add_cast(self, uuid, _):
|
||||
"""Handle zeroconf discovery of a new chromecast."""
|
||||
discover_chromecast(hass, browser.devices[uuid], config_entry)
|
||||
|
||||
@override
|
||||
def update_cast(self, uuid, _):
|
||||
"""Handle zeroconf discovery of an updated chromecast."""
|
||||
discover_chromecast(hass, browser.devices[uuid], config_entry)
|
||||
|
||||
@override
|
||||
def remove_cast(self, uuid, service, cast_info):
|
||||
"""Handle zeroconf discovery of a removed chromecast."""
|
||||
_remove_chromecast(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import configparser
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
from typing import TYPE_CHECKING, ClassVar, override
|
||||
from urllib.parse import urlparse
|
||||
from uuid import UUID
|
||||
|
||||
@@ -180,37 +180,45 @@ class CastStatusListener(
|
||||
if not cast_device._cast_info.is_audio_group: # noqa: SLF001
|
||||
self._mz_mgr.register_listener(chromecast.uuid, self)
|
||||
|
||||
@override
|
||||
def new_cast_status(self, status):
|
||||
"""Handle reception of a new CastStatus."""
|
||||
if self._valid:
|
||||
self._cast_device.new_cast_status(status)
|
||||
|
||||
@override
|
||||
def new_media_status(self, status):
|
||||
"""Handle reception of a new MediaStatus."""
|
||||
if self._valid:
|
||||
self._cast_device.new_media_status(status)
|
||||
|
||||
@override
|
||||
def load_media_failed(self, queue_item_id, error_code):
|
||||
"""Handle reception of a new MediaStatus."""
|
||||
if self._valid:
|
||||
self._cast_device.load_media_failed(queue_item_id, error_code)
|
||||
|
||||
@override
|
||||
def new_connection_status(self, status):
|
||||
"""Handle reception of a new ConnectionStatus."""
|
||||
if self._valid:
|
||||
self._cast_device.new_connection_status(status)
|
||||
|
||||
@override
|
||||
def added_to_multizone(self, group_uuid):
|
||||
"""Handle the cast added to a group."""
|
||||
|
||||
@override
|
||||
def removed_from_multizone(self, group_uuid):
|
||||
"""Handle the cast removed from a group."""
|
||||
if self._valid:
|
||||
self._cast_device.multizone_new_media_status(group_uuid, None)
|
||||
|
||||
@override
|
||||
def multizone_new_cast_status(self, group_uuid, cast_status):
|
||||
"""Handle reception of a new CastStatus for a group."""
|
||||
|
||||
@override
|
||||
def multizone_new_media_status(self, group_uuid, media_status):
|
||||
"""Handle reception of a new MediaStatus for a group."""
|
||||
if self._valid:
|
||||
|
||||
@@ -6,7 +6,7 @@ from datetime import datetime
|
||||
from functools import wraps
|
||||
import json
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Concatenate
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, override
|
||||
|
||||
import pychromecast.config
|
||||
import pychromecast.const
|
||||
@@ -338,6 +338,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
]:
|
||||
self._attr_device_class = MediaPlayerDeviceClass.SPEAKER
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Create chromecast object when added to hass."""
|
||||
self._async_setup(self.entity_id)
|
||||
@@ -346,6 +347,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self.hass, SIGNAL_HASS_CAST_SHOW_VIEW, self._handle_signal_show_view
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect Chromecast object when removed."""
|
||||
await self._async_tear_down()
|
||||
@@ -354,6 +356,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self._cast_view_remove_handler()
|
||||
self._cast_view_remove_handler = None
|
||||
|
||||
@override
|
||||
async def _async_connect_to_chromecast(self):
|
||||
"""Set up the chromecast object."""
|
||||
await super()._async_connect_to_chromecast()
|
||||
@@ -363,6 +366,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self.media_status = self._chromecast.media_controller.status
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
async def _async_disconnect(self):
|
||||
"""Disconnect Chromecast object if it is set."""
|
||||
await super()._async_disconnect()
|
||||
@@ -370,6 +374,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self._attr_available = False
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
def _invalidate(self):
|
||||
"""Invalidate some attributes."""
|
||||
super()._invalidate()
|
||||
@@ -528,6 +533,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
"""Start an app."""
|
||||
self._get_chromecast().start_app(app_id)
|
||||
|
||||
@override
|
||||
def turn_on(self) -> None:
|
||||
"""Turn on the cast device."""
|
||||
|
||||
@@ -547,51 +553,60 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
else:
|
||||
self._start_app(pychromecast.config.APP_MEDIA_RECEIVER)
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def turn_off(self) -> None:
|
||||
"""Turn off the cast device."""
|
||||
self._get_chromecast().quit_app()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def mute_volume(self, mute: bool) -> None:
|
||||
"""Mute the volume."""
|
||||
self._get_chromecast().set_volume_muted(mute)
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
self._get_chromecast().set_volume(volume)
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_play(self) -> None:
|
||||
"""Send play command."""
|
||||
media_controller = self._media_controller()
|
||||
media_controller.play()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_pause(self) -> None:
|
||||
"""Send pause command."""
|
||||
media_controller = self._media_controller()
|
||||
media_controller.pause()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_stop(self) -> None:
|
||||
"""Send stop command."""
|
||||
media_controller = self._media_controller()
|
||||
media_controller.stop()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_previous_track(self) -> None:
|
||||
"""Send previous track command."""
|
||||
media_controller = self._media_controller()
|
||||
media_controller.queue_prev()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_next_track(self) -> None:
|
||||
"""Send next track command."""
|
||||
media_controller = self._media_controller()
|
||||
media_controller.queue_next()
|
||||
|
||||
@override
|
||||
@api_error
|
||||
def media_seek(self, position: float) -> None:
|
||||
"""Seek the media to a specific location."""
|
||||
@@ -636,6 +651,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
children=sorted(children, key=lambda c: c.title),
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
@@ -675,6 +691,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
self.hass, media_content_id, content_filter=content_filter
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
@@ -816,6 +833,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return (media_status, media_status_received)
|
||||
|
||||
@override
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
@@ -858,6 +876,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return MediaPlayerState.IDLE
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_content_id(self) -> str | None:
|
||||
"""Content ID of current playing media."""
|
||||
@@ -867,6 +886,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.content_id if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_content_type(self) -> MediaType | None:
|
||||
"""Content type of current playing media."""
|
||||
@@ -891,6 +911,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return MediaType.VIDEO
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_duration(self):
|
||||
"""Duration of current playing media in seconds."""
|
||||
@@ -900,6 +921,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.duration if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
@@ -910,64 +932,75 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return images[0].url if images and images[0].url else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.title if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_artist(self):
|
||||
"""Artist of current playing media (Music track only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.artist if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_album_name(self):
|
||||
"""Album of current playing media (Music track only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.album_name if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_album_artist(self):
|
||||
"""Album artist of current playing media (Music track only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.album_artist if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_track(self):
|
||||
"""Track number of current playing media (Music track only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.track if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_series_title(self):
|
||||
"""Return the title of the series of current playing media."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.series_title if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_season(self):
|
||||
"""Season of current playing media (TV Show only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.season if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_episode(self):
|
||||
"""Episode of current playing media (TV Show only)."""
|
||||
media_status = self._media_status()[0]
|
||||
return media_status.episode if media_status else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def app_id(self):
|
||||
"""Return the ID of the current running app."""
|
||||
return self._chromecast.app_id if self._chromecast else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def app_name(self):
|
||||
"""Name of the current running app."""
|
||||
return self._chromecast.app_display_name if self._chromecast else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def supported_features(self) -> MediaPlayerEntityFeature:
|
||||
"""Flag media player features that are supported."""
|
||||
@@ -1006,6 +1039,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
|
||||
return support
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_position(self):
|
||||
"""Position of current playing media in seconds."""
|
||||
@@ -1021,6 +1055,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||
return None
|
||||
return media_status.current_time
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_position_updated_at(self):
|
||||
"""When was the position of the current playing media valid.
|
||||
@@ -1075,6 +1110,7 @@ class DynamicCastGroup(CastDevice):
|
||||
"""Create chromecast object."""
|
||||
self._async_setup("Dynamic group")
|
||||
|
||||
@override
|
||||
async def _async_cast_removed(self, discover: ChromecastInfo):
|
||||
"""Handle removal of Chromecast."""
|
||||
if self._cast_info.uuid != discover.uuid:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Climate device for CCM15 coordinator."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from ccm15 import CCM15DeviceState, CCM15SlaveDevice
|
||||
|
||||
@@ -93,6 +93,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
"""Return device data."""
|
||||
return self.coordinator.get_ac_data(self._ac_index)
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_temperature(self) -> int | None:
|
||||
"""Return current temperature."""
|
||||
@@ -100,6 +101,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return data.temperature
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature(self) -> int | None:
|
||||
"""Return target temperature."""
|
||||
@@ -107,6 +109,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return data.temperature_setpoint
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return hvac mode."""
|
||||
@@ -115,6 +118,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return CONST_CMD_STATE_MAP[mode]
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return fan mode."""
|
||||
@@ -123,6 +127,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return CONST_CMD_FAN_MAP[mode]
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return swing mode."""
|
||||
@@ -130,11 +135,13 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return SWING_ON if data.is_swing_on else SWING_OFF
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return the availability of the entity."""
|
||||
return self.data is not None
|
||||
|
||||
@override
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return the optional state attributes."""
|
||||
@@ -142,6 +149,7 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
return {"error_code": data.error_code}
|
||||
return {}
|
||||
|
||||
@override
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set the target temperature."""
|
||||
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is not None:
|
||||
@@ -149,18 +157,22 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
|
||||
self._ac_index, self.data, temperature, kwargs.get(ATTR_HVAC_MODE)
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set the hvac mode."""
|
||||
await self.coordinator.async_set_hvac_mode(self._ac_index, self.data, hvac_mode)
|
||||
|
||||
@override
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set the fan mode."""
|
||||
await self.coordinator.async_set_fan_mode(self._ac_index, self.data, fan_mode)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@override
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn on."""
|
||||
await self.async_set_hvac_mode(HVACMode.AUTO)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for Midea ccm15 AC Controller integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from ccm15 import CCM15Device
|
||||
import voluptuous as vol
|
||||
@@ -27,6 +27,7 @@ class CCM15ConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from ccm15 import CCM15Device, CCM15DeviceState, CCM15SlaveDevice
|
||||
import httpx
|
||||
@@ -44,6 +45,7 @@ class CCM15Coordinator(DataUpdateCoordinator[CCM15DeviceState]):
|
||||
"""Get the host."""
|
||||
return self._host
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> CCM15DeviceState:
|
||||
"""Fetch data from Rain Bird device."""
|
||||
try:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for the CentriConnect/MyPropane API integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from aiocentriconnect import CentriConnect
|
||||
from aiocentriconnect.exceptions import (
|
||||
@@ -58,6 +58,7 @@ class CentriConnectConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -6,6 +6,7 @@ Responsible for polling the device API endpoint and normalizing data for entitie
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from aiocentriconnect import CentriConnect, Tank
|
||||
from aiocentriconnect.exceptions import CentriConnectConnectionError, CentriConnectError
|
||||
@@ -65,6 +66,7 @@ class CentriConnectCoordinator(DataUpdateCoordinator[Tank]):
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_setup(self) -> None:
|
||||
try:
|
||||
tank_data = await self.api_client.async_get_tank_data()
|
||||
@@ -79,6 +81,7 @@ class CentriConnectCoordinator(DataUpdateCoordinator[Tank]):
|
||||
tank_size_unit=tank_data.tank_size_unit,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> Tank:
|
||||
"""Fetch device state."""
|
||||
try:
|
||||
|
||||
@@ -4,6 +4,7 @@ from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
EntityCategory,
|
||||
@@ -236,6 +237,7 @@ class CentriConnectSensor(CentriConnectBaseEntity, SensorEntity):
|
||||
|
||||
entity_description: CentriConnectSensorEntityDescription
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> StateType | datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Config flow for the Cert Expiry platform."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
@@ -19,8 +18,6 @@ from .errors import (
|
||||
)
|
||||
from .helper import get_cert_expiry_timestamp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
@@ -56,6 +53,7 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return True
|
||||
return False
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self,
|
||||
user_input: Mapping[str, Any] | None = None,
|
||||
@@ -75,9 +73,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
title=title,
|
||||
data={CONF_HOST: host, CONF_PORT: port},
|
||||
)
|
||||
if self.source == SOURCE_IMPORT:
|
||||
_LOGGER.error("Config import failed for %s", user_input[CONF_HOST])
|
||||
return self.async_abort(reason="import_failed")
|
||||
else:
|
||||
user_input = {}
|
||||
user_input[CONF_HOST] = ""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -46,6 +47,7 @@ class CertExpiryDataUpdateCoordinator(DataUpdateCoordinator[datetime | None]):
|
||||
always_update=False,
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> datetime | None:
|
||||
"""Fetch certificate."""
|
||||
try:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Counter for the days until an HTTPS (TLS) certificate will expire."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
@@ -12,6 +12,7 @@ class CertExpiryEntity(CoordinatorEntity[CertExpiryDataUpdateCoordinator]):
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
@override
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return additional sensor state attributes."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Counter for the days until an HTTPS (TLS) certificate will expire."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import override
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -44,6 +45,7 @@ class SSLCertificateTimestamp(CertExpiryEntity, SensorEntity):
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
)
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> datetime | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"import_failed": "Import from config failed",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for chacon_dio integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
from dio_chacon_wifi_api.exceptions import DIOChaconAPIError, DIOChaconInvalidAuthError
|
||||
@@ -25,6 +25,7 @@ DATA_SCHEMA = vol.Schema(
|
||||
class ChaconDioConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for chacon_dio."""
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Cover Platform for Chacon Dio REV-SHUTTER devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum, ShutterMoveEnum
|
||||
|
||||
@@ -49,6 +49,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
|
||||
@override
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recompute the attribute values on init or state change."""
|
||||
self._attr_available = data["connected"]
|
||||
@@ -57,6 +58,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
self._attr_is_opening = data["movement"] == ShutterMoveEnum.UP.value
|
||||
self._attr_is_closed = self._attr_current_cover_position == 0
|
||||
|
||||
@override
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover.
|
||||
|
||||
@@ -80,6 +82,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
self.target_id, ShutterMoveEnum.DOWN
|
||||
)
|
||||
|
||||
@override
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover.
|
||||
|
||||
@@ -101,6 +104,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.UP)
|
||||
|
||||
@override
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
|
||||
@@ -112,6 +116,7 @@ class ChaconDioCover(ChaconDioEntity, CoverEntity):
|
||||
|
||||
await self.client.move_shutter_direction(self.target_id, ShutterMoveEnum.STOP)
|
||||
|
||||
@override
|
||||
async def async_set_cover_position(self, **kwargs: Any) -> None:
|
||||
"""Set the cover open position in percentage.
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Base entity for the Chacon Dio entity."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from dio_chacon_wifi_api import DIOChaconAPIClient
|
||||
|
||||
@@ -38,6 +38,7 @@ class ChaconDioEntity(Entity):
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recomputes the attributes values."""
|
||||
|
||||
@override
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register the callback for server side events."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Switch Platform for Chacon Dio REV-LIGHT and switch plug devices."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from dio_chacon_wifi_api.const import DeviceTypeEnum
|
||||
|
||||
@@ -38,11 +38,13 @@ class ChaconDioSwitch(ChaconDioEntity, SwitchEntity):
|
||||
_attr_device_class = SwitchDeviceClass.SWITCH
|
||||
_attr_name = None
|
||||
|
||||
@override
|
||||
def _update_attr(self, data: dict[str, Any]) -> None:
|
||||
"""Recompute the attribute values on init or state change."""
|
||||
self._attr_available = data["connected"]
|
||||
self._attr_is_on = data["is_on"]
|
||||
|
||||
@override
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the switch.
|
||||
|
||||
@@ -59,6 +61,7 @@ class ChaconDioSwitch(ChaconDioEntity, SwitchEntity):
|
||||
|
||||
await self.client.switch_switch(self.target_id, True)
|
||||
|
||||
@override
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the switch.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Support for interfacing with an instance of getchannels.com."""
|
||||
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from pychannels import Channels
|
||||
import voluptuous as vol
|
||||
@@ -138,11 +138,13 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
self.now_playing_summary = None
|
||||
self.now_playing_image_url = None
|
||||
|
||||
@override
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the name of the player."""
|
||||
return self._name
|
||||
|
||||
@override
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
"""Return the state of the player."""
|
||||
@@ -162,21 +164,25 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
self.update_favorite_channels()
|
||||
self.update_state(self.client.status())
|
||||
|
||||
@override
|
||||
@property
|
||||
def source_list(self):
|
||||
"""List of favorite channels."""
|
||||
return [channel["name"] for channel in self.favorite_channels]
|
||||
|
||||
@override
|
||||
@property
|
||||
def is_volume_muted(self):
|
||||
"""Boolean if volume is currently muted."""
|
||||
return self.muted
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_content_id(self):
|
||||
"""Content ID of current playing channel."""
|
||||
return self.channel_number
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_image_url(self):
|
||||
"""Image url of current playing media."""
|
||||
@@ -187,6 +193,7 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
|
||||
return "https://getchannels.com/assets/img/icon-1024.png"
|
||||
|
||||
@override
|
||||
@property
|
||||
def media_title(self):
|
||||
"""Title of current playing media."""
|
||||
@@ -195,38 +202,45 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
|
||||
return None
|
||||
|
||||
@override
|
||||
def mute_volume(self, mute: bool) -> None:
|
||||
"""Mute (true) or unmute (false) player."""
|
||||
if mute != self.muted:
|
||||
response = self.client.toggle_muted()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def media_stop(self) -> None:
|
||||
"""Send media_stop command to player."""
|
||||
self.status = "stopped"
|
||||
response = self.client.stop()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def media_play(self) -> None:
|
||||
"""Send media_play command to player."""
|
||||
response = self.client.resume()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def media_pause(self) -> None:
|
||||
"""Send media_pause command to player."""
|
||||
response = self.client.pause()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def media_next_track(self) -> None:
|
||||
"""Seek ahead."""
|
||||
response = self.client.skip_forward()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def media_previous_track(self) -> None:
|
||||
"""Seek back."""
|
||||
response = self.client.skip_backward()
|
||||
self.update_state(response)
|
||||
|
||||
@override
|
||||
def select_source(self, source: str) -> None:
|
||||
"""Select a channel to tune to."""
|
||||
for channel in self.favorite_channels:
|
||||
@@ -235,6 +249,7 @@ class ChannelsPlayer(MediaPlayerEntity):
|
||||
self.update_state(response)
|
||||
break
|
||||
|
||||
@override
|
||||
def play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
) -> None:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Config flow for the Chess.com integration."""
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from typing import Any, override
|
||||
|
||||
from chess_com_api import ChessComClient, NotFoundError
|
||||
import voluptuous as vol
|
||||
@@ -18,6 +18,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class ChessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Chess.com."""
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import override
|
||||
|
||||
from chess_com_api import ChessComAPIError, ChessComClient, Player, PlayerStats
|
||||
|
||||
@@ -45,6 +46,7 @@ class ChessCoordinator(DataUpdateCoordinator[ChessData]):
|
||||
)
|
||||
self.client = ChessComClient(session=async_get_clientsession(hass))
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> ChessData:
|
||||
"""Update data from Chess.com."""
|
||||
try:
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from chess_com_api import PlayerStats
|
||||
|
||||
@@ -121,6 +121,7 @@ class ChessPlayerSensor(ChessEntity, SensorEntity):
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}.{description.key}"
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
@@ -148,6 +149,7 @@ class ChessGameModeSensor(ChessEntity, SensorEntity):
|
||||
)
|
||||
self._attr_translation_key = f"{game_mode}_{description.translation_key}"
|
||||
|
||||
@override
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the state of the sensor."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
from typing import Any, Concatenate
|
||||
from typing import Any, Concatenate, override
|
||||
|
||||
from cieloconnectapi.exceptions import AuthenticationError
|
||||
|
||||
@@ -103,6 +103,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
super().__init__(coordinator, device_id)
|
||||
self._attr_unique_id = device_id
|
||||
|
||||
@override
|
||||
@property
|
||||
def temperature_unit(self) -> str:
|
||||
"""Return the unit of temperature in Home Assistant format.
|
||||
@@ -124,6 +125,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
|
||||
return UnitOfTemperature.CELSIUS
|
||||
|
||||
@override
|
||||
@property
|
||||
def supported_features(self) -> ClimateEntityFeature:
|
||||
"""Return dynamic feature flags based on the current mode."""
|
||||
@@ -147,6 +149,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
|
||||
return flags
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_humidity(self) -> int | None:
|
||||
"""Return the current humidity, if available."""
|
||||
@@ -154,58 +157,69 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
return self.device_data.humidity
|
||||
return None
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature_low(self) -> float | None:
|
||||
"""Return the low target temperature for HEAT_COOL mode."""
|
||||
return self.client.target_temperature_low(self.temperature_unit)
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature_high(self) -> float | None:
|
||||
"""Return the high target temperature for HEAT_COOL mode."""
|
||||
return self.client.target_temperature_high(self.temperature_unit)
|
||||
|
||||
@override
|
||||
@property
|
||||
def hvac_mode(self) -> HVACMode | None:
|
||||
"""Return the current HVAC mode."""
|
||||
mode = self.client.hvac_mode()
|
||||
return CIELO_TO_HA_HVAC.get(mode, mode)
|
||||
|
||||
@override
|
||||
@property
|
||||
def hvac_modes(self) -> list[HVACMode]:
|
||||
"""Return the list of available HVAC modes."""
|
||||
modes = self.client.hvac_modes() or []
|
||||
return [CIELO_TO_HA_HVAC.get(m, m) for m in modes]
|
||||
|
||||
@override
|
||||
@property
|
||||
def current_temperature(self) -> float | None:
|
||||
"""Return the current indoor temperature."""
|
||||
return self.client.current_temperature()
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature(self) -> float | None:
|
||||
"""Return the target temperature."""
|
||||
return self.client.target_temperature()
|
||||
|
||||
@override
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
"""Return the minimum possible target temperature."""
|
||||
return self.client.min_temp()
|
||||
|
||||
@override
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
"""Return the maximum possible target temperature."""
|
||||
return self.client.max_temp()
|
||||
|
||||
@override
|
||||
@property
|
||||
def target_temperature_step(self) -> float | None:
|
||||
"""Return the precision of the thermostat."""
|
||||
return self.client.target_temperature_step(self.temperature_unit)
|
||||
|
||||
@override
|
||||
@property
|
||||
def fan_mode(self) -> str | None:
|
||||
"""Return the current fan mode."""
|
||||
return self.client.fan_mode()
|
||||
|
||||
@override
|
||||
@property
|
||||
def fan_modes(self) -> list[str] | None:
|
||||
"""Return the list of available fan modes.
|
||||
@@ -217,6 +231,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
"""
|
||||
return self.client.fan_modes()
|
||||
|
||||
@override
|
||||
@property
|
||||
def swing_modes(self) -> list[str] | None:
|
||||
"""Return the list of available swing modes.
|
||||
@@ -228,11 +243,13 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
"""
|
||||
return self.client.swing_modes()
|
||||
|
||||
@override
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
return self.client.preset_mode()
|
||||
|
||||
@override
|
||||
@property
|
||||
def preset_modes(self) -> list[str] | None:
|
||||
"""Return the list of available preset modes.
|
||||
@@ -244,16 +261,19 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
"""
|
||||
return self.client.preset_modes()
|
||||
|
||||
@override
|
||||
@property
|
||||
def swing_mode(self) -> str | None:
|
||||
"""Return the current swing mode."""
|
||||
return self.device_data.swing_mode if self.device_data else None
|
||||
|
||||
@override
|
||||
@property
|
||||
def precision(self) -> float:
|
||||
"""Return the precision of the thermostat."""
|
||||
return self.client.precision(self.temperature_unit)
|
||||
|
||||
@override
|
||||
@async_handle_api_call
|
||||
async def async_set_temperature(self, **kwargs: Any) -> None:
|
||||
"""Set new target temperature."""
|
||||
@@ -270,27 +290,32 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
**{ATTR_TEMPERATURE: kwargs.get(ATTR_TEMPERATURE)},
|
||||
)
|
||||
|
||||
@override
|
||||
@async_handle_api_call
|
||||
async def async_set_fan_mode(self, fan_mode: str) -> None:
|
||||
"""Set new fan mode."""
|
||||
return await self.client.async_set_fan_mode(fan_mode)
|
||||
|
||||
@override
|
||||
@async_handle_api_call
|
||||
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||
"""Set new preset mode."""
|
||||
return await self.client.async_set_preset_mode(preset_mode)
|
||||
|
||||
@override
|
||||
@async_handle_api_call
|
||||
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
|
||||
"""Set new HVAC mode."""
|
||||
cielo_mode = HA_TO_CIELO_HVAC.get(hvac_mode)
|
||||
return await self.client.async_set_hvac_mode(cielo_mode)
|
||||
|
||||
@override
|
||||
@async_handle_api_call
|
||||
async def async_set_swing_mode(self, swing_mode: str) -> None:
|
||||
"""Set new swing mode."""
|
||||
return await self.client.async_set_swing_mode(swing_mode)
|
||||
|
||||
@override
|
||||
async def async_turn_on(self) -> None:
|
||||
"""Turn the climate device on."""
|
||||
modes = self.hvac_modes or []
|
||||
@@ -303,6 +328,7 @@ class CieloClimate(CieloDeviceEntity, ClimateEntity):
|
||||
|
||||
raise HomeAssistantError("No non-off HVAC modes available to turn on device")
|
||||
|
||||
@override
|
||||
async def async_turn_off(self) -> None:
|
||||
"""Turn the climate device off."""
|
||||
await self.async_set_hvac_mode(HVACMode.OFF)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Config Flow for Cielo integration."""
|
||||
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, override
|
||||
|
||||
from aiohttp import ClientError
|
||||
from cieloconnectapi import CieloClient
|
||||
@@ -61,6 +61,7 @@ class CieloConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return client.user_id, {CONF_TOKEN: token}
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from typing import Any, Final
|
||||
from typing import Any, Final, override
|
||||
|
||||
from aiohttp import ClientError
|
||||
from cieloconnectapi import CieloClient
|
||||
@@ -59,6 +59,7 @@ class CieloDataUpdateCoordinator(DataUpdateCoordinator[CieloData]):
|
||||
),
|
||||
)
|
||||
|
||||
@override
|
||||
async def _async_update_data(self) -> CieloData:
|
||||
"""Fetch data from the API."""
|
||||
try:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user