mirror of
https://github.com/home-assistant/core.git
synced 2026-05-24 09:45:13 +02:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff43e12449 | |||
| 7dfef5c82a | |||
| b75cd0f6a7 | |||
| 7859aba432 |
@@ -15,7 +15,6 @@ 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,7 +25,6 @@ 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@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
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@bdca6f102f98e2b4c7029491a53dfd366469e33d # v2.0.4
|
||||
uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3
|
||||
with:
|
||||
extra-args: --all-files zizmor
|
||||
|
||||
|
||||
@@ -28,11 +28,11 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
@@ -15,7 +15,6 @@ 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
-2
@@ -466,6 +466,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/electrasmart/ @jafar-atili
|
||||
/homeassistant/components/electric_kiwi/ @mikey0000
|
||||
/tests/components/electric_kiwi/ @mikey0000
|
||||
/homeassistant/components/electrolux/ @electrolux-oss
|
||||
/tests/components/electrolux/ @electrolux-oss
|
||||
/homeassistant/components/elevenlabs/ @sorgfresser
|
||||
/tests/components/elevenlabs/ @sorgfresser
|
||||
/homeassistant/components/elgato/ @frenck
|
||||
@@ -1413,8 +1415,8 @@ CLAUDE.md @home-assistant/core
|
||||
/tests/components/pushover/ @engrbm87
|
||||
/homeassistant/components/pvoutput/ @frenck
|
||||
/tests/components/pvoutput/ @frenck
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
|
||||
/homeassistant/components/pvpc_hourly_pricing/ @azogue
|
||||
/tests/components/pvpc_hourly_pricing/ @azogue
|
||||
/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"] is not FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] != FlowResultType.CREATE_ENTRY:
|
||||
return result
|
||||
|
||||
# we got final result
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"service": "mdi:dialpad"
|
||||
},
|
||||
"alarm_toggle_chime": {
|
||||
"service": "mdi:bell-ring"
|
||||
"service": "mdi:abc"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ 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,9 +102,6 @@
|
||||
"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}"
|
||||
},
|
||||
|
||||
@@ -5,12 +5,8 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import labs, websocket_api
|
||||
from homeassistant.components.hassio import HassioNotReadyError
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import discovery_flow
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -53,7 +49,6 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
DATA_COMPONENT: HassKey[Analytics] = HassKey(DOMAIN)
|
||||
_DATA_SNAPSHOTS_URL: HassKey[str | None] = HassKey(f"{DOMAIN}_snapshots_url")
|
||||
|
||||
LABS_SNAPSHOT_FEATURE = "snapshots"
|
||||
|
||||
@@ -62,39 +57,18 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the analytics integration."""
|
||||
analytics_config = config.get(DOMAIN, {})
|
||||
|
||||
snapshots_url: str | None = None
|
||||
if CONF_SNAPSHOTS_URL in analytics_config:
|
||||
await labs.async_update_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, enabled=True
|
||||
)
|
||||
snapshots_url = analytics_config[CONF_SNAPSHOTS_URL]
|
||||
else:
|
||||
snapshots_url = None
|
||||
|
||||
hass.data[_DATA_SNAPSHOTS_URL] = snapshots_url
|
||||
|
||||
discovery_flow.async_create_flow(
|
||||
hass, DOMAIN, context={"source": SOURCE_SYSTEM}, data={}
|
||||
)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Analytics from a config entry."""
|
||||
snapshots_url = hass.data.get(_DATA_SNAPSHOTS_URL)
|
||||
analytics = Analytics(hass, snapshots_url)
|
||||
|
||||
try:
|
||||
await analytics.load()
|
||||
except HassioNotReadyError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="supervisor_not_ready",
|
||||
) from err
|
||||
# Load stored data
|
||||
await analytics.load()
|
||||
|
||||
started = False
|
||||
|
||||
@@ -106,30 +80,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if started:
|
||||
await analytics.async_schedule()
|
||||
|
||||
async def start_schedule(hass: HomeAssistant) -> None:
|
||||
"""Start the send schedule once Home Assistant has started."""
|
||||
async def start_schedule(_event: Event) -> None:
|
||||
"""Start the send schedule after the started event."""
|
||||
nonlocal started
|
||||
started = True
|
||||
await analytics.async_schedule()
|
||||
|
||||
entry.async_on_unload(
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
)
|
||||
labs.async_subscribe_preview_feature(
|
||||
hass, DOMAIN, LABS_SNAPSHOT_FEATURE, _async_handle_labs_update
|
||||
)
|
||||
entry.async_on_unload(async_at_started(hass, start_schedule))
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_schedule)
|
||||
|
||||
websocket_api.async_register_command(hass, websocket_analytics)
|
||||
websocket_api.async_register_command(hass, websocket_analytics_preferences)
|
||||
|
||||
hass.http.register_view(AnalyticsDevicesView)
|
||||
|
||||
hass.data[DATA_COMPONENT] = analytics
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload an Analytics config entry."""
|
||||
analytics = hass.data.pop(DATA_COMPONENT)
|
||||
analytics.cancel_scheduled()
|
||||
return True
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command({vol.Required("type"): "analytics"})
|
||||
@@ -139,9 +109,7 @@ def websocket_analytics(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Return analytics preferences."""
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
connection.send_result(
|
||||
msg["id"],
|
||||
{ATTR_PREFERENCES: analytics.preferences, ATTR_ONBOARDED: analytics.onboarded},
|
||||
@@ -162,10 +130,8 @@ async def websocket_analytics_preferences(
|
||||
msg: dict[str, Any],
|
||||
) -> None:
|
||||
"""Update analytics preferences."""
|
||||
if (analytics := hass.data.get(DATA_COMPONENT)) is None:
|
||||
connection.send_error(msg["id"], websocket_api.ERR_NOT_FOUND, "Not loaded")
|
||||
return
|
||||
preferences = msg[ATTR_PREFERENCES]
|
||||
analytics = hass.data[DATA_COMPONENT]
|
||||
|
||||
await analytics.save_preferences(preferences)
|
||||
await analytics.async_schedule()
|
||||
|
||||
@@ -299,8 +299,12 @@ class Analytics:
|
||||
self._data = AnalyticsData.from_dict(stored)
|
||||
|
||||
if self.supervisor and not self.onboarded:
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable.
|
||||
# The caller is responsible for handling this and triggering a retry.
|
||||
# This may raise HassioNotReadyError if Supervisor was unreachable
|
||||
# during setup of the Supervisor integration. That will fail setup
|
||||
# of this integration. However there is no better option at this time
|
||||
# since we need to get the diagnostic setting from Supervisor to correctly
|
||||
# setup this integration and we can't raise ConfigEntryNotReady to
|
||||
# trigger a retry from async_setup.
|
||||
supervisor_info = hassio.get_supervisor_info(self._hass)
|
||||
|
||||
# User have not configured analytics, get this setting from the supervisor
|
||||
@@ -345,7 +349,8 @@ class Analytics:
|
||||
await self._save()
|
||||
|
||||
if self.supervisor:
|
||||
# The others may raise HassioNotReadyError if only some
|
||||
# get_supervisor_info was called during setup so we can't get here
|
||||
# if it raised. The others may raise HassioNotReadyError if only some
|
||||
# data was successfully fetched from Supervisor
|
||||
supervisor_info = hassio.get_supervisor_info(hass)
|
||||
with contextlib.suppress(hassio.HassioNotReadyError):
|
||||
@@ -625,16 +630,6 @@ class Analytics:
|
||||
err,
|
||||
)
|
||||
|
||||
@callback
|
||||
def cancel_scheduled(self) -> None:
|
||||
"""Cancel all scheduled analytics tasks."""
|
||||
if self._basic_scheduled is not None:
|
||||
self._basic_scheduled()
|
||||
self._basic_scheduled = None
|
||||
if self._snapshot_scheduled is not None:
|
||||
self._snapshot_scheduled()
|
||||
self._snapshot_scheduled = None
|
||||
|
||||
async def async_schedule(self) -> None:
|
||||
"""Schedule analytics."""
|
||||
if not self.onboarded:
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
"""Config flow for Analytics integration."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class AnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Analytics."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_system(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
return self.async_create_entry(title="Analytics", data={})
|
||||
@@ -3,7 +3,6 @@
|
||||
"name": "Analytics",
|
||||
"after_dependencies": ["energy", "hassio", "recorder"],
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["api", "websocket_api", "http"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/analytics",
|
||||
"integration_type": "system",
|
||||
@@ -15,6 +14,5 @@
|
||||
"report_issue_url": "https://github.com/OHF-Device-Database/device-database/issues/new"
|
||||
}
|
||||
},
|
||||
"quality_scale": "internal",
|
||||
"single_config_entry": true
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
{
|
||||
"exceptions": {
|
||||
"supervisor_not_ready": {
|
||||
"message": "Supervisor was not ready during setup, will retry"
|
||||
}
|
||||
},
|
||||
"preview_features": {
|
||||
"snapshots": {
|
||||
"description": "We're creating the [Open Home Foundation Device Database](https://www.home-assistant.io/blog/2026/02/02/about-device-database/): a free, open source community-powered resource to help users find practical information about how smart home devices perform in real installations.\n\nYou can help us build it by opting in to share anonymized data about your devices. This data will only ever include device-specific details (like model or manufacturer) – never personally identifying information (like the names you assign).\n\nFind out how we process your data (should you choose to contribute) in our [Data Use Statement](https://www.openhomefoundation.org/device-database-data-use-statement).",
|
||||
|
||||
@@ -226,7 +226,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Set initial options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
if self._get_entry().state != 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 is AOSmithOperationMode.VACATION
|
||||
supported_mode.mode == 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 is AOSmithOperationMode.VACATION
|
||||
return self.device.status.current_mode == 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 is DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model == 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 is KeyboardFocusState.Focused)
|
||||
self._update_state(atv.keyboard.text_focus_state == 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 is KeyboardFocusState.Focused)
|
||||
self._update_state(new_state == 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 is DeviceModel.Unknown and dev_info.raw_model
|
||||
if dev_info.model == 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 is PairingRequirement.Unsupported:
|
||||
if service.pairing == PairingRequirement.Unsupported:
|
||||
_LOGGER.debug("%s does not support pairing", self.protocol)
|
||||
return await self.async_pair_next_protocol()
|
||||
if service.pairing is PairingRequirement.Disabled:
|
||||
if service.pairing == PairingRequirement.Disabled:
|
||||
return await self.async_step_protocol_disabled()
|
||||
if service.pairing is PairingRequirement.NotNeeded:
|
||||
if service.pairing == 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 is Protocol.DMAP:
|
||||
if self.protocol == 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 is not FeatureState.Unsupported:
|
||||
if feature_info and feature_info.state != 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 is PowerState.Off
|
||||
and self.atv.power.power_state == 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 is DeviceState.Playing:
|
||||
if state == 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 is not ShuffleState.Off
|
||||
return self._playing.shuffle != 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 is PowerState.On
|
||||
or self.atv.power.power_state == 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 is not KeyboardFocusState.Focused:
|
||||
if focus_state != KeyboardFocusState.Focused:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="keyboard_not_focused",
|
||||
|
||||
@@ -263,9 +263,9 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
|
||||
def media_channel(self) -> str | None:
|
||||
"""Channel currently playing."""
|
||||
source = self._state.get_source()
|
||||
if source is SourceCodes.DAB:
|
||||
if source == SourceCodes.DAB:
|
||||
value = self._state.get_dab_station()
|
||||
elif source is SourceCodes.FM:
|
||||
elif source == 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() is SourceCodes.DAB:
|
||||
if self._state.get_source() == SourceCodes.DAB:
|
||||
value = self._state.get_dls_pdt()
|
||||
else:
|
||||
value = None
|
||||
|
||||
@@ -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 is not intent.IntentResponseType.ACTION_DONE
|
||||
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
|
||||
or not intent_response.matched_states
|
||||
):
|
||||
return False
|
||||
|
||||
@@ -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"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return {
|
||||
key: val for key, val in result.items() if key not in ("result", "data")
|
||||
}
|
||||
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] != 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"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] != 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"] is data_entry_flow.FlowResultType.FORM
|
||||
result["type"] == 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"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return dict(result)
|
||||
if result["type"] is not data_entry_flow.FlowResultType.FORM:
|
||||
if result["type"] != data_entry_flow.FlowResultType.FORM:
|
||||
return result # type: ignore[return-value]
|
||||
|
||||
data = dict(result)
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"bluetooth-adapters==2.1.0",
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.29.11",
|
||||
"dbus-fast==5.0.3",
|
||||
"habluetooth==6.2.0"
|
||||
"dbus-fast==5.0.0",
|
||||
"habluetooth==6.1.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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 is added_event else "remove"
|
||||
event_type = "add" if registration.event == added_event else "remove"
|
||||
_async_event_message({event_type: [registration.scanner.details]})
|
||||
|
||||
manager = _get_manager(hass)
|
||||
|
||||
@@ -158,10 +158,7 @@ def process_service_info(
|
||||
)
|
||||
|
||||
# If payload is encrypted and the bindkey is not verified then we need to reauth
|
||||
if (
|
||||
data.encryption_scheme is not EncryptionScheme.NONE
|
||||
and not data.bindkey_verified
|
||||
):
|
||||
if data.encryption_scheme != 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 is EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme == 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 is EncryptionScheme.BTHOME_BINDKEY:
|
||||
if discovery.device.encryption_scheme == 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 is EncryptionScheme.BTHOME_BINDKEY:
|
||||
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
|
||||
return await self.async_step_get_encryption_key()
|
||||
|
||||
# Otherwise there wasn't actually encryption so abort
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Config flow for the Cert Expiry platform."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT
|
||||
|
||||
from .const import DEFAULT_PORT, DOMAIN
|
||||
@@ -18,6 +19,8 @@ from .errors import (
|
||||
)
|
||||
from .helper import get_cert_expiry_timestamp
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow."""
|
||||
@@ -72,6 +75,9 @@ 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 @@
|
||||
"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": {
|
||||
|
||||
@@ -110,7 +110,7 @@ class ComelitAlarmEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if alarm is available."""
|
||||
if self._area.human_status is AlarmAreaState.UNKNOWN:
|
||||
if self._area.human_status == AlarmAreaState.UNKNOWN:
|
||||
return False
|
||||
return super().available
|
||||
|
||||
@@ -124,7 +124,7 @@ class ComelitAlarmEntity(
|
||||
self._area.human_status,
|
||||
self._area.armed,
|
||||
)
|
||||
if self._area.human_status is AlarmAreaState.ARMED:
|
||||
if self._area.human_status == AlarmAreaState.ARMED:
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
|
||||
return AlarmControlPanelState.ARMED_AWAY
|
||||
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
|
||||
|
||||
@@ -43,7 +43,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: cast(ComelitVedoAreaObject, obj).anomaly,
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoAreaObject, obj).human_status is not AlarmAreaState.UNKNOWN
|
||||
cast(ComelitVedoAreaObject, obj).human_status != AlarmAreaState.UNKNOWN
|
||||
),
|
||||
),
|
||||
ComelitBinarySensorEntityDescription(
|
||||
@@ -67,7 +67,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
|
||||
object_type=ALARM_ZONE,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
is_on_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status is AlarmZoneState.FAULTY
|
||||
cast(ComelitVedoZoneObject, obj).human_status == AlarmZoneState.FAULTY
|
||||
),
|
||||
available_fn=lambda obj: (
|
||||
cast(ComelitVedoZoneObject, obj).human_status
|
||||
|
||||
@@ -65,9 +65,11 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
except aiocomelit_exceptions.CannotAuthenticate as err:
|
||||
# pylint: disable-next=home-assistant-exception-placeholder-mismatch
|
||||
raise InvalidAuth(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_authenticate",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
) from err
|
||||
finally:
|
||||
await api.logout()
|
||||
|
||||
@@ -166,12 +166,12 @@ class ComelitVedoSensorEntity(
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Sensor availability."""
|
||||
return self._zone_object.human_status is not AlarmZoneState.UNAVAILABLE
|
||||
return self._zone_object.human_status != AlarmZoneState.UNAVAILABLE
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Sensor value."""
|
||||
if (status := self._zone_object.human_status) is AlarmZoneState.UNKNOWN:
|
||||
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
|
||||
return None
|
||||
|
||||
return cast(str, status.value)
|
||||
|
||||
@@ -148,7 +148,7 @@ def _prepare_config_flow_result_json(
|
||||
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
"""Convert result to JSON."""
|
||||
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
|
||||
return prepare_result_json(result)
|
||||
|
||||
data = {key: val for key, val in result.items() if key not in ("data", "context")}
|
||||
|
||||
@@ -646,7 +646,7 @@ class DefaultAgent(ConversationEntity):
|
||||
cache_value = self._intent_cache.get(cache_key)
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage is IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
cache_value.stage == IntentMatchingStage.EXPOSED_ENTITIES_ONLY
|
||||
):
|
||||
_LOGGER.debug("Got cached result for exposed entities")
|
||||
return cache_value.result
|
||||
@@ -686,7 +686,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unexposed_entities_match = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage is IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
cache_value.stage == IntentMatchingStage.UNEXPOSED_ENTITIES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for all entities")
|
||||
return cache_value.result
|
||||
@@ -731,7 +731,7 @@ class DefaultAgent(ConversationEntity):
|
||||
skip_unknown_names = False
|
||||
if cache_value is not None:
|
||||
if (cache_value.result is not None) and (
|
||||
cache_value.stage is IntentMatchingStage.UNKNOWN_NAMES
|
||||
cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES
|
||||
):
|
||||
_LOGGER.debug("Got cached result for unknown names")
|
||||
return cache_value.result
|
||||
@@ -1447,7 +1447,7 @@ class DefaultAgent(ConversationEntity):
|
||||
|
||||
response = await self._async_process_intent_result(result, user_input, chat_log)
|
||||
if (
|
||||
response.response_type is intent.IntentResponseType.ERROR
|
||||
response.response_type == intent.IntentResponseType.ERROR
|
||||
and response.error_code
|
||||
not in (
|
||||
intent.IntentResponseErrorCode.FAILED_TO_HANDLE,
|
||||
@@ -1546,7 +1546,7 @@ def _get_match_error_response(
|
||||
# device_class only
|
||||
return ErrorKey.NO_DEVICE_CLASS, {"device_class": device_class}
|
||||
|
||||
if (reason is intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
if (reason == intent.MatchFailedReason.DOMAIN) and constraints.domains:
|
||||
domain = next(iter(constraints.domains)) # first domain
|
||||
if constraints.area_name:
|
||||
# domain in area
|
||||
@@ -1565,7 +1565,7 @@ def _get_match_error_response(
|
||||
# domain only
|
||||
return ErrorKey.NO_DOMAIN, {"domain": domain}
|
||||
|
||||
if reason is intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if reason == intent.MatchFailedReason.DUPLICATE_NAME:
|
||||
if constraints.floor_name:
|
||||
# duplicate on floor
|
||||
return ErrorKey.DUPLICATE_ENTITIES_IN_FLOOR, {
|
||||
@@ -1582,26 +1582,26 @@ def _get_match_error_response(
|
||||
|
||||
return ErrorKey.DUPLICATE_ENTITIES, {"entity": result.no_match_name}
|
||||
|
||||
if reason is intent.MatchFailedReason.INVALID_AREA:
|
||||
if reason == intent.MatchFailedReason.INVALID_AREA:
|
||||
# Invalid area name
|
||||
return ErrorKey.NO_AREA, {"area": result.no_match_name}
|
||||
|
||||
if reason is intent.MatchFailedReason.INVALID_FLOOR:
|
||||
if reason == intent.MatchFailedReason.INVALID_FLOOR:
|
||||
# Invalid floor name
|
||||
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
|
||||
|
||||
if reason is intent.MatchFailedReason.FEATURE:
|
||||
if reason == intent.MatchFailedReason.FEATURE:
|
||||
# Feature not supported by entity
|
||||
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
|
||||
|
||||
if reason is intent.MatchFailedReason.STATE:
|
||||
if reason == intent.MatchFailedReason.STATE:
|
||||
# Entity is not in correct state
|
||||
assert constraints.states
|
||||
state = next(iter(constraints.states))
|
||||
|
||||
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
|
||||
|
||||
if reason is intent.MatchFailedReason.ASSISTANT:
|
||||
if reason == intent.MatchFailedReason.ASSISTANT:
|
||||
# Not exposed
|
||||
if constraints.name:
|
||||
if constraints.area_name:
|
||||
|
||||
@@ -61,7 +61,7 @@ class CurrencylayerSensor(SensorEntity):
|
||||
"""Implementing the Currencylayer sensor."""
|
||||
|
||||
_attr_attribution = "Data provided by currencylayer.com"
|
||||
_attr_icon = "mdi:currency-usd"
|
||||
_attr_icon = "mdi:currency"
|
||||
|
||||
def __init__(self, rest: CurrencylayerData, base: str, quote: str) -> None:
|
||||
"""Initialize the sensor."""
|
||||
|
||||
@@ -26,12 +26,12 @@ from homeassistant.components.climate import (
|
||||
HVACAction,
|
||||
HVACMode,
|
||||
)
|
||||
from homeassistant.const import ATTR_LOCKED, ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import DeconzConfigEntry
|
||||
from .const import ATTR_OFFSET, ATTR_VALVE
|
||||
from .const import ATTR_LOCKED, ATTR_OFFSET, ATTR_VALVE
|
||||
from .entity import DeconzDevice
|
||||
from .hub import DeconzHub
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
ATTR_DARK = "dark"
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_LOCKED = "locked"
|
||||
ATTR_OFFSET = "offset"
|
||||
ATTR_ON = "on"
|
||||
ATTR_VALVE = "valve"
|
||||
|
||||
@@ -80,7 +80,7 @@ async def async_validate_device_automation_config(
|
||||
# the checks below which look for a config entry matching the device automation
|
||||
# domain
|
||||
if (
|
||||
automation_type is DeviceAutomationType.ACTION
|
||||
automation_type == DeviceAutomationType.ACTION
|
||||
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
|
||||
):
|
||||
# Pass the unvalidated config to avoid mutating the raw config twice
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
"""Provide functionality to keep track of devices."""
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import ATTR_GPS_ACCURACY, STATE_HOME # noqa: F401
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .config_entry import ( # noqa: F401
|
||||
DATA_COMPONENT,
|
||||
BaseScannerEntity,
|
||||
BaseTrackerEntity,
|
||||
ScannerEntity,
|
||||
ScannerEntityDescription,
|
||||
TrackerEntity,
|
||||
@@ -40,8 +33,6 @@ from .const import ( # noqa: F401
|
||||
DEFAULT_TRACK_NEW,
|
||||
DOMAIN,
|
||||
ENTITY_ID_FORMAT,
|
||||
LOGGER,
|
||||
PLATFORM_TYPE_LEGACY,
|
||||
SCAN_INTERVAL,
|
||||
SourceType,
|
||||
)
|
||||
@@ -53,9 +44,7 @@ from .legacy import ( # noqa: F401
|
||||
SOURCE_TYPES,
|
||||
AsyncSeeCallback,
|
||||
DeviceScanner,
|
||||
DeviceTracker,
|
||||
SeeCallback,
|
||||
async_create_platform_type,
|
||||
async_setup_integration as async_setup_legacy_integration,
|
||||
see,
|
||||
)
|
||||
@@ -68,43 +57,5 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the device tracker."""
|
||||
component = hass.data[DATA_COMPONENT] = EntityComponent[BaseTrackerEntity](
|
||||
LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
||||
)
|
||||
component.config = {}
|
||||
component.register_shutdown()
|
||||
|
||||
# The tracker is loaded in the async_setup_legacy_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None:
|
||||
return
|
||||
|
||||
if platform.type != PLATFORM_TYPE_LEGACY:
|
||||
await component.async_setup_platform(p_type, {}, info)
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
async_setup_legacy_integration(hass, config, tracker_future),
|
||||
eager_start=True,
|
||||
)
|
||||
async_setup_legacy_integration(hass, config)
|
||||
return True
|
||||
|
||||
@@ -37,7 +37,11 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers import (
|
||||
config_validation as cv,
|
||||
discovery,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
async_track_time_interval,
|
||||
async_track_utc_time_change,
|
||||
@@ -200,7 +204,40 @@ def see(
|
||||
hass.services.call(DOMAIN, SERVICE_SEE, data)
|
||||
|
||||
|
||||
async def async_setup_integration(
|
||||
@callback
|
||||
def async_setup_integration(hass: HomeAssistant, config: ConfigType) -> None:
|
||||
"""Set up the legacy integration."""
|
||||
# The tracker is loaded in the _async_setup_integration task so
|
||||
# we create a future to avoid waiting on it here so that only
|
||||
# async_platform_discovered will have to wait in the rare event
|
||||
# a custom component still uses the legacy device tracker discovery.
|
||||
tracker_future: asyncio.Future[DeviceTracker] = hass.loop.create_future()
|
||||
|
||||
async def async_platform_discovered(
|
||||
p_type: str, info: dict[str, Any] | None
|
||||
) -> None:
|
||||
"""Load a platform."""
|
||||
platform = await async_create_platform_type(hass, config, p_type, {})
|
||||
|
||||
if platform is None or platform.type != PLATFORM_TYPE_LEGACY:
|
||||
return
|
||||
|
||||
tracker = await tracker_future
|
||||
await platform.async_setup_legacy(hass, tracker, info)
|
||||
|
||||
discovery.async_listen_platform(hass, DOMAIN, async_platform_discovered)
|
||||
#
|
||||
# Legacy and platforms load in a non-awaited tracked task
|
||||
# to ensure device tracker setup can continue and config
|
||||
# entry integrations are not waiting for legacy device
|
||||
# tracker platforms to be set up.
|
||||
#
|
||||
hass.async_create_task(
|
||||
_async_setup_integration(hass, config, tracker_future), eager_start=True
|
||||
)
|
||||
|
||||
|
||||
async def _async_setup_integration(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
tracker_future: asyncio.Future[DeviceTracker],
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"quality_scale": "internal",
|
||||
"requirements": [
|
||||
"aiodhcpwatcher==1.2.1",
|
||||
"aiodiscover==3.2.3",
|
||||
"aiodiscover==3.2.0",
|
||||
"cached-ipaddress==1.0.1"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -281,7 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
await self._device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -290,7 +290,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
@@ -718,7 +718,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
|
||||
# If already playing, or don't want to autoplay, no need to call Play
|
||||
autoplay = extra.get("autoplay", True)
|
||||
if self._device.transport_state is TransportState.PLAYING or not autoplay:
|
||||
if self._device.transport_state == TransportState.PLAYING or not autoplay:
|
||||
return
|
||||
|
||||
# Play it
|
||||
@@ -748,7 +748,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
|
||||
@@ -782,10 +782,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
|
||||
if not (play_mode := self._device.play_mode):
|
||||
return None
|
||||
|
||||
if play_mode is PlayMode.VENDOR_DEFINED:
|
||||
if play_mode == PlayMode.VENDOR_DEFINED:
|
||||
return None
|
||||
|
||||
if play_mode is PlayMode.REPEAT_ONE:
|
||||
if play_mode == PlayMode.REPEAT_ONE:
|
||||
return RepeatMode.ONE
|
||||
|
||||
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
|
||||
|
||||
@@ -236,7 +236,7 @@ class DmsDeviceSource:
|
||||
except KeyError, ValueError:
|
||||
bootid = None
|
||||
|
||||
if change is ssdp.SsdpChange.UPDATE:
|
||||
if change == ssdp.SsdpChange.UPDATE:
|
||||
# This is an announcement that bootid is about to change
|
||||
if self._bootid is not None and self._bootid == bootid:
|
||||
# Store the new value (because our old value matches) so that we
|
||||
@@ -258,7 +258,7 @@ class DmsDeviceSource:
|
||||
await self.device_disconnect()
|
||||
self._bootid = bootid
|
||||
|
||||
if change is ssdp.SsdpChange.BYEBYE:
|
||||
if change == ssdp.SsdpChange.BYEBYE:
|
||||
# Device is going away
|
||||
if self._device:
|
||||
# Disconnect from gone device
|
||||
@@ -267,7 +267,7 @@ class DmsDeviceSource:
|
||||
self._ssdp_connect_failed = False
|
||||
|
||||
if (
|
||||
change is ssdp.SsdpChange.ALIVE
|
||||
change == ssdp.SsdpChange.ALIVE
|
||||
and not self._device
|
||||
and not self._ssdp_connect_failed
|
||||
):
|
||||
|
||||
@@ -6,6 +6,7 @@ import logging
|
||||
|
||||
import aiodns
|
||||
from aiodns.error import DNSError
|
||||
from pycares import AresError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PORT
|
||||
@@ -77,7 +78,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: DnsIPConfigEntry) -> boo
|
||||
) from err
|
||||
|
||||
errors = [
|
||||
result for result in results if isinstance(result, (TimeoutError, DNSError))
|
||||
result
|
||||
for result in results
|
||||
if isinstance(
|
||||
result, (TimeoutError, DNSError, AresError, asyncio.CancelledError)
|
||||
)
|
||||
]
|
||||
if errors and len(errors) == len(results):
|
||||
await _close_resolvers()
|
||||
|
||||
@@ -8,12 +8,6 @@
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"download_dir": "Download directory"
|
||||
},
|
||||
"data_description": {
|
||||
"download_dir": "The directory where downloaded files will be stored. This can be an absolute path or a path relative to the Home Assistant configuration directory."
|
||||
},
|
||||
"description": "Select a location to get to store downloads. The setup will check if the directory exists."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
class EcobeeBaseEntity(Entity):
|
||||
"""Base methods for Ecobee entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
|
||||
"""Initiate base methods for Ecobee entities."""
|
||||
self.data = data
|
||||
|
||||
@@ -1,11 +1,4 @@
|
||||
{
|
||||
"entity": {
|
||||
"number": {
|
||||
"fan_min_on_time": {
|
||||
"default": "mdi:fan-clock"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"create_vacation": {
|
||||
"service": "mdi:umbrella-beach"
|
||||
|
||||
@@ -24,6 +24,7 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
|
||||
"""Implement the notification entity for the Ecobee thermostat."""
|
||||
|
||||
_attr_name = None
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, data: EcobeeData, thermostat_index: int) -> None:
|
||||
"""Initialize the thermostat."""
|
||||
|
||||
@@ -74,10 +74,6 @@ async def async_setup_entry(
|
||||
)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
EcobeeFanMinOnTime(data, index) for index in range(len(data.ecobee.thermostats))
|
||||
)
|
||||
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
@@ -90,6 +86,7 @@ class EcobeeVentilatorMinTime(EcobeeBaseEntity, NumberEntity):
|
||||
_attr_native_max_value = 60
|
||||
_attr_native_step = 5
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -133,6 +130,7 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
|
||||
"""
|
||||
|
||||
_attr_device_class = NumberDeviceClass.TEMPERATURE
|
||||
_attr_has_entity_name = True
|
||||
_attr_icon = "mdi:thermometer-off"
|
||||
_attr_mode = NumberMode.BOX
|
||||
_attr_native_min_value = -25
|
||||
@@ -167,39 +165,3 @@ class EcobeeCompressorMinTemp(EcobeeBaseEntity, NumberEntity):
|
||||
"""Set new compressor minimum temperature."""
|
||||
self.data.ecobee.set_aux_cutover_threshold(self.thermostat_index, value)
|
||||
self.update_without_throttle = True
|
||||
|
||||
|
||||
class EcobeeFanMinOnTime(EcobeeBaseEntity, NumberEntity):
|
||||
"""Minimum minutes per hour that the fan must run on an ecobee thermostat."""
|
||||
|
||||
_attr_native_min_value = 0
|
||||
_attr_native_max_value = 60
|
||||
_attr_native_step = 5
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
_attr_translation_key = "fan_min_on_time"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data: EcobeeData,
|
||||
thermostat_index: int,
|
||||
) -> None:
|
||||
"""Initialize ecobee fan minimum on time."""
|
||||
super().__init__(data, thermostat_index)
|
||||
self._attr_unique_id = f"{self.base_unique_id}_fan_min_on_time"
|
||||
self.update_without_throttle = False
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest state from the thermostat."""
|
||||
if self.update_without_throttle:
|
||||
await self.data.update(no_throttle=True)
|
||||
self.update_without_throttle = False
|
||||
else:
|
||||
await self.data.update()
|
||||
self._attr_native_value = self.thermostat["settings"]["fanMinOnTime"]
|
||||
|
||||
def set_native_value(self, value: float) -> None:
|
||||
"""Set new fan minimum on time value."""
|
||||
step = self._attr_native_step
|
||||
aligned_value = int(round(value / step) * step)
|
||||
self.data.ecobee.set_fan_min_on_time(self.thermostat_index, aligned_value)
|
||||
self.update_without_throttle = True
|
||||
|
||||
@@ -37,9 +37,6 @@
|
||||
"compressor_protection_min_temp": {
|
||||
"name": "Compressor minimum temperature"
|
||||
},
|
||||
"fan_min_on_time": {
|
||||
"name": "Fan minimum on time"
|
||||
},
|
||||
"ventilator_min_type_away": {
|
||||
"name": "Ventilator minimum time away"
|
||||
},
|
||||
|
||||
@@ -53,6 +53,7 @@ async def async_setup_entry(
|
||||
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
"""Represent 20 min timer for an ecobee thermostat with ventilator."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_name = "Ventilator 20m Timer"
|
||||
|
||||
def __init__(
|
||||
@@ -103,6 +104,7 @@ class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
|
||||
class EcobeeSwitchAuxHeatOnly(EcobeeBaseEntity, SwitchEntity):
|
||||
"""Representation of a aux_heat_only ecobee switch."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_translation_key = "aux_heat_only"
|
||||
|
||||
def __init__(
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
"""The Electrolux integration."""
|
||||
|
||||
from asyncio import CancelledError
|
||||
from collections.abc import Awaitable, Callable
|
||||
import logging
|
||||
|
||||
from electrolux_group_developer_sdk.auth.token_manager import TokenManager
|
||||
from electrolux_group_developer_sdk.client.appliance_client import ApplianceClient
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.client_exception import (
|
||||
ApplianceClientException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DOMAIN, NEW_APPLIANCE_SIGNAL, USER_AGENT
|
||||
from .coordinator import (
|
||||
ElectroluxConfigEntry,
|
||||
ElectroluxData,
|
||||
ElectroluxDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ElectroluxConfigEntry) -> bool:
|
||||
"""Set up Electrolux integration entry."""
|
||||
|
||||
token_manager = create_token_manager(hass, entry)
|
||||
client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
|
||||
try:
|
||||
await client.test_connection()
|
||||
except BadCredentialsException as e:
|
||||
raise ConfigEntryAuthFailed("Bad credentials detected.") from e
|
||||
except FailedConnectionException as e:
|
||||
raise ConfigEntryNotReady("Connection with client failed.") from e
|
||||
|
||||
try:
|
||||
appliances = await fetch_appliance_data(client)
|
||||
except ApplianceClientException as e:
|
||||
raise ConfigEntryNotReady from e
|
||||
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator] = {}
|
||||
on_livestream_opening_callback_list: list[Callable[[], Awaitable[None]]] = []
|
||||
|
||||
async def check_for_new_devices_callback() -> None:
|
||||
"""Trigger _check_for_new_devices asynchronously."""
|
||||
await _check_for_new_devices(
|
||||
hass, entry, client, on_livestream_opening_callback_list
|
||||
)
|
||||
|
||||
on_livestream_opening_callback_list.append(check_for_new_devices_callback)
|
||||
|
||||
for appliance in appliances:
|
||||
appliance_id = appliance.appliance.applianceId
|
||||
coordinator = ElectroluxDataUpdateCoordinator(
|
||||
hass, entry, client=client, appliance_id=appliance_id
|
||||
)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
# Subscribe this coordinator to its appliance events
|
||||
coordinator.add_client_listener()
|
||||
|
||||
coordinators[appliance_id] = coordinator
|
||||
# Device state is refreshed whenever the SSE connection opens.
|
||||
on_livestream_opening_callback_list.append(coordinator.async_refresh)
|
||||
|
||||
sse_task = entry.async_create_background_task(
|
||||
hass,
|
||||
client.start_event_stream(on_livestream_opening_callback_list),
|
||||
"electrolux event listener",
|
||||
)
|
||||
|
||||
entry.runtime_data = ElectroluxData(
|
||||
client=client,
|
||||
appliances=appliances,
|
||||
coordinators=coordinators,
|
||||
sse_task=sse_task,
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ElectroluxConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
# Remove SSE listeners
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
for coordinator in coordinators.values():
|
||||
coordinator.remove_client_listeners()
|
||||
|
||||
# Cancel SSE task
|
||||
sse_task = entry.runtime_data.sse_task
|
||||
sse_task.cancel()
|
||||
try:
|
||||
await sse_task
|
||||
except CancelledError:
|
||||
_LOGGER.info("SSE stream cancelled for entry %s", entry.entry_id)
|
||||
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
def create_token_manager(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
) -> TokenManager:
|
||||
"""Create a token manager for the Electrolux integration."""
|
||||
|
||||
def save_tokens(new_access: str, new_refresh: str, new_api_key: str) -> None:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry,
|
||||
data={
|
||||
**entry.data,
|
||||
CONF_API_KEY: new_api_key,
|
||||
CONF_ACCESS_TOKEN: new_access,
|
||||
CONF_REFRESH_TOKEN: new_refresh,
|
||||
},
|
||||
)
|
||||
|
||||
api_key = entry.data.get(CONF_API_KEY)
|
||||
refresh_token = entry.data.get(CONF_REFRESH_TOKEN)
|
||||
access_token = entry.data.get(CONF_ACCESS_TOKEN)
|
||||
|
||||
if access_token and refresh_token and api_key:
|
||||
return TokenManager(access_token, refresh_token, api_key, save_tokens)
|
||||
raise ConfigEntryAuthFailed("Missing access token, refresh token or API key")
|
||||
|
||||
|
||||
async def _check_for_new_devices(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
client: ApplianceClient,
|
||||
on_livestream_opening_callback_list: list[Callable[[], Awaitable[None]]],
|
||||
) -> None:
|
||||
"""Fetch appliances from API and trigger discovery for any new ones."""
|
||||
_LOGGER.info("Checking for new devices")
|
||||
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
appliances = await fetch_appliance_data(client)
|
||||
entry.runtime_data.appliances = appliances
|
||||
|
||||
existing_ids = set(coordinators.keys())
|
||||
|
||||
for appliance in appliances:
|
||||
appliance_id = appliance.appliance.applianceId
|
||||
# Detect NEW appliances
|
||||
if appliance_id not in existing_ids:
|
||||
# Create coordinator for appliance
|
||||
coordinator = ElectroluxDataUpdateCoordinator(
|
||||
hass, entry, client=client, appliance_id=appliance_id
|
||||
)
|
||||
|
||||
await coordinator.async_refresh()
|
||||
|
||||
coordinator.add_client_listener()
|
||||
coordinators[appliance_id] = coordinator
|
||||
on_livestream_opening_callback_list.append(coordinator.async_refresh)
|
||||
|
||||
# Notify all platforms
|
||||
async_dispatcher_send(
|
||||
hass, f"{NEW_APPLIANCE_SIGNAL}_{entry.entry_id}", appliance
|
||||
)
|
||||
|
||||
# Detect MISSING appliances
|
||||
discovered_ids = {appliance.appliance.applianceId for appliance in appliances}
|
||||
missing_ids = existing_ids - discovered_ids
|
||||
device_registry = dr.async_get(hass)
|
||||
for missing_id in missing_ids:
|
||||
_LOGGER.warning("Appliance %s no longer found, removing", missing_id)
|
||||
|
||||
# Remove coordinator
|
||||
coordinator = coordinators.pop(missing_id)
|
||||
coordinator.remove_client_listeners()
|
||||
on_livestream_opening_callback_list.remove(coordinator.async_refresh)
|
||||
|
||||
device_entry = device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, missing_id)}
|
||||
)
|
||||
|
||||
if device_entry:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=entry.entry_id
|
||||
)
|
||||
|
||||
|
||||
async def fetch_appliance_data(client: ApplianceClient) -> list[ApplianceData]:
|
||||
"""Helper method to retrieve all the appliances data from the Electrolux APIs."""
|
||||
try:
|
||||
appliances = await client.get_appliance_data()
|
||||
except ApplianceClientException as e:
|
||||
_LOGGER.warning("Failed to get appliances: %s", e)
|
||||
raise
|
||||
|
||||
# Filter out appliances where details or state is None
|
||||
return [
|
||||
appliance
|
||||
for appliance in appliances
|
||||
if appliance.details is not None and appliance.state is not None
|
||||
]
|
||||
@@ -0,0 +1,99 @@
|
||||
"""Config flow for Electrolux integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from electrolux_group_developer_sdk.auth.invalid_credentials_exception import (
|
||||
InvalidCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.auth.token_manager import TokenManager
|
||||
from electrolux_group_developer_sdk.client.appliance_client import ApplianceClient
|
||||
from electrolux_group_developer_sdk.client.bad_credentials_exception import (
|
||||
BadCredentialsException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.failed_connection_exception import (
|
||||
FailedConnectionException,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_API_KEY
|
||||
|
||||
from .const import CONF_REFRESH_TOKEN, DOMAIN, USER_AGENT
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for the Electrolux integration."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step of the config flow."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
token_manager: TokenManager
|
||||
email: str
|
||||
try:
|
||||
token_manager = await _authenticate_user(user_input)
|
||||
client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
email = (await client.get_user_email()).email
|
||||
except InvalidCredentialsException, BadCredentialsException:
|
||||
errors["base"] = "invalid_auth"
|
||||
except FailedConnectionException:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
await self.async_set_unique_id(token_manager.get_user_id())
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=f"Electrolux for {email}",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self._show_form(step_id="user", errors=errors)
|
||||
|
||||
def _show_form(self, step_id: str, errors: dict[str, str]) -> ConfigFlowResult:
|
||||
return self.async_show_form(
|
||||
step_id=step_id,
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_ACCESS_TOKEN): str,
|
||||
vol.Required(CONF_REFRESH_TOKEN): str,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"portal_link": "https://developer.electrolux.one/generateToken"
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def _authenticate_user(user_input: Mapping[str, Any]) -> TokenManager:
|
||||
token_manager = TokenManager(
|
||||
access_token=user_input[CONF_ACCESS_TOKEN],
|
||||
refresh_token=user_input[CONF_REFRESH_TOKEN],
|
||||
api_key=user_input[CONF_API_KEY],
|
||||
)
|
||||
|
||||
token_manager.ensure_credentials()
|
||||
|
||||
appliance_client = ApplianceClient(
|
||||
token_manager=token_manager, external_user_agent=USER_AGENT
|
||||
)
|
||||
|
||||
# Test a connection in the config flow
|
||||
await appliance_client.test_connection()
|
||||
|
||||
return token_manager
|
||||
@@ -0,0 +1,11 @@
|
||||
"""Constants for Electrolux integration."""
|
||||
|
||||
from homeassistant.const import __version__ as HA_VERSION
|
||||
|
||||
DOMAIN = "electrolux"
|
||||
|
||||
CONF_REFRESH_TOKEN = "refresh_token"
|
||||
|
||||
NEW_APPLIANCE_SIGNAL = "electrolux_new_appliance"
|
||||
|
||||
USER_AGENT = f"HomeAssistant/{HA_VERSION}"
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Electrolux coordinator class."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from asyncio import Task
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliance_client import (
|
||||
ApplianceClient,
|
||||
apply_sse_update,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.client_exception import (
|
||||
ApplianceClientException,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.dto.appliance_state import ApplianceState
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True)
|
||||
class ElectroluxData:
|
||||
"""Electrolux data type."""
|
||||
|
||||
client: ApplianceClient
|
||||
appliances: list[ApplianceData]
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator]
|
||||
sse_task: Task
|
||||
|
||||
|
||||
type ElectroluxConfigEntry = ConfigEntry[ElectroluxData]
|
||||
|
||||
|
||||
class ElectroluxDataUpdateCoordinator(DataUpdateCoordinator[ApplianceState]):
|
||||
"""Class for fetching appliance data from the API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ElectroluxConfigEntry,
|
||||
client: ApplianceClient,
|
||||
appliance_id: str,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self.client = client
|
||||
self._appliance_id = appliance_id
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"{DOMAIN}_{config_entry.entry_id}_{appliance_id}",
|
||||
update_interval=None,
|
||||
always_update=False,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> ApplianceState:
|
||||
"""Return the current appliance state (SSE keeps it updated)."""
|
||||
try:
|
||||
appliance_state = await self.client.get_appliance_state(self._appliance_id)
|
||||
except ValueError as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
except ApplianceClientException as exception:
|
||||
raise UpdateFailed(exception) from exception
|
||||
else:
|
||||
return appliance_state
|
||||
|
||||
def add_client_listener(self) -> None:
|
||||
"""Register an SSE listener to the appliance client for appliance state updates."""
|
||||
self.client.add_listener(self._appliance_id, self.callback_handle_event)
|
||||
|
||||
def remove_client_listeners(self) -> None:
|
||||
"""Remove all SSE listeners."""
|
||||
self.client.remove_all_listeners_by_appliance_id(self._appliance_id)
|
||||
|
||||
def callback_handle_event(self, event: dict) -> None:
|
||||
"""Handle an incoming SSE event. Event will look like: {"userId": "...", "applianceId": "...", "property": "timeToEnd", "value": 720}."""
|
||||
|
||||
current_state = self.data
|
||||
if not current_state:
|
||||
return
|
||||
|
||||
updated_state = apply_sse_update(
|
||||
current_state,
|
||||
event,
|
||||
)
|
||||
|
||||
self.async_set_updated_data(updated_state)
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Base entity for Electrolux integration."""
|
||||
|
||||
from abc import abstractmethod
|
||||
import logging
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ElectroluxDataUpdateCoordinator
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ElectroluxBaseEntity[T: ApplianceData](
|
||||
CoordinatorEntity[ElectroluxDataUpdateCoordinator]
|
||||
):
|
||||
"""Base class for Electrolux entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: T,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
unique_id_suffix: str,
|
||||
) -> None:
|
||||
"""Initialize the base device."""
|
||||
super().__init__(coordinator)
|
||||
appliance_name = appliance_data.appliance.applianceName
|
||||
appliance_id = appliance_data.appliance.applianceId
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert appliance_data.details
|
||||
assert appliance_data.state
|
||||
|
||||
appliance_info = appliance_data.details.applianceInfo
|
||||
|
||||
self._appliance_data = appliance_data
|
||||
self._attr_unique_id = f"{appliance_id}_{unique_id_suffix}"
|
||||
self._appliance_id = appliance_id
|
||||
self._appliance_capabilities = appliance_data.details.capabilities
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, appliance_id)},
|
||||
name=appliance_name,
|
||||
manufacturer=appliance_info.brand,
|
||||
model=appliance_info.model,
|
||||
serial_number=appliance_info.serialNumber,
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""When entity is added to HA."""
|
||||
await super().async_added_to_hass()
|
||||
self._handle_coordinator_update()
|
||||
|
||||
@abstractmethod
|
||||
def _update_attr_state(self) -> bool:
|
||||
"""Update entity-specific attributes. Returns True if any attributes were changed, otherwise False."""
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""When the coordinator updates."""
|
||||
appliance_state = self.coordinator.data
|
||||
if not appliance_state:
|
||||
_LOGGER.warning("Appliance %s not found in update", self._appliance_id)
|
||||
return
|
||||
|
||||
# Update state
|
||||
self._appliance_data.update_state(appliance_state)
|
||||
state_changed = self._update_attr_state()
|
||||
|
||||
if state_changed:
|
||||
self.async_write_ha_state()
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Contains entity helper methods."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import NEW_APPLIANCE_SIGNAL
|
||||
from .coordinator import ElectroluxConfigEntry, ElectroluxDataUpdateCoordinator
|
||||
from .entity import ElectroluxBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entities_helper(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
build_entities_fn: Callable[
|
||||
[ApplianceData, dict[str, ElectroluxDataUpdateCoordinator]],
|
||||
list[ElectroluxBaseEntity],
|
||||
],
|
||||
) -> None:
|
||||
"""Provide async_setup_entry helper."""
|
||||
|
||||
appliances: list[ApplianceData] = entry.runtime_data.appliances
|
||||
coordinators = entry.runtime_data.coordinators
|
||||
|
||||
entities: list[ElectroluxBaseEntity] = []
|
||||
|
||||
for appliance_data in appliances:
|
||||
entities.extend(build_entities_fn(appliance_data, coordinators))
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
# Listen for new/removed appliances
|
||||
async def _new_appliance(appliance_data: ApplianceData):
|
||||
new_entities = build_entities_fn(appliance_data, coordinators)
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_dispatcher_connect(
|
||||
hass, f"{NEW_APPLIANCE_SIGNAL}_{entry.entry_id}", _new_appliance
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"appliance_state": {
|
||||
"default": "mdi:information-outline"
|
||||
},
|
||||
"food_probe_state": {
|
||||
"default": "mdi:thermometer-probe"
|
||||
},
|
||||
"food_probe_temperature": {
|
||||
"default": "mdi:thermometer-probe"
|
||||
},
|
||||
"remote_control": {
|
||||
"default": "mdi:remote"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "electrolux",
|
||||
"name": "Electrolux",
|
||||
"codeowners": ["@electrolux-oss"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/electrolux",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["electrolux-group-developer-sdk==0.5.0"]
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions are implemented currently.
|
||||
appropriate-polling:
|
||||
status: exempt
|
||||
comment: |
|
||||
Polling is only performed on infrequent events (when the livestream of events is opened, in order to sync),
|
||||
otherwise the integration works via push
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: |
|
||||
No actions are implemented currently.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
|
||||
# Gold
|
||||
devices: todo
|
||||
diagnostics: todo
|
||||
discovery-update-info: todo
|
||||
discovery: todo
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: todo
|
||||
strict-typing: todo
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Sensor entity for Electrolux Integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import cast
|
||||
|
||||
from electrolux_group_developer_sdk.client.appliances.appliance_data import (
|
||||
ApplianceData,
|
||||
)
|
||||
from electrolux_group_developer_sdk.client.appliances.cr_appliance import CRAppliance
|
||||
from electrolux_group_developer_sdk.client.appliances.ov_appliance import OVAppliance
|
||||
from electrolux_group_developer_sdk.feature_constants import (
|
||||
APPLIANCE_STATE,
|
||||
DISPLAY_FOOD_PROBE_TEMPERATURE_C,
|
||||
DISPLAY_FOOD_PROBE_TEMPERATURE_F,
|
||||
DISPLAY_TEMPERATURE_C,
|
||||
DISPLAY_TEMPERATURE_F,
|
||||
FOOD_PROBE_STATE,
|
||||
REMOTE_CONTROL,
|
||||
)
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
StateType,
|
||||
)
|
||||
from homeassistant.const import UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .coordinator import ElectroluxConfigEntry, ElectroluxDataUpdateCoordinator
|
||||
from .entity import ElectroluxBaseEntity
|
||||
from .entity_helper import async_setup_entities_helper
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ELECTROLUX_TO_HA_TEMPERATURE_UNIT = {
|
||||
"CELSIUS": UnitOfTemperature.CELSIUS,
|
||||
"FAHRENHEIT": UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ElectroluxSensorDescription(SensorEntityDescription):
|
||||
"""Custom sensor description for Electrolux sensors."""
|
||||
|
||||
value_fn: Callable[..., StateType]
|
||||
exists_fn: Callable[[ApplianceData], bool] = lambda *args: True
|
||||
feature_name: str | None = None
|
||||
known_values: set[str] | None = None
|
||||
|
||||
|
||||
OVEN_ELECTROLUX_SENSORS: tuple[ElectroluxSensorDescription, ...] = (
|
||||
ElectroluxSensorDescription(
|
||||
key="appliance_state",
|
||||
translation_key="appliance_state",
|
||||
value_fn=lambda appliance: appliance.get_current_appliance_state(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=APPLIANCE_STATE,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(APPLIANCE_STATE),
|
||||
known_values={
|
||||
"alarm",
|
||||
"delayed_start",
|
||||
"end_of_cycle",
|
||||
"idle",
|
||||
"off",
|
||||
"paused",
|
||||
"ready_to_start",
|
||||
"running",
|
||||
},
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="food_probe_state",
|
||||
translation_key="food_probe_state",
|
||||
value_fn=lambda appliance: appliance.get_current_food_probe_insertion_state(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=FOOD_PROBE_STATE,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(FOOD_PROBE_STATE),
|
||||
known_values={
|
||||
"inserted",
|
||||
"not_inserted",
|
||||
},
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="remote_control",
|
||||
translation_key="remote_control",
|
||||
value_fn=lambda appliance: appliance.get_current_remote_control(),
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
feature_name=REMOTE_CONTROL,
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(REMOTE_CONTROL),
|
||||
known_values={
|
||||
"disabled",
|
||||
"enabled",
|
||||
"not_safety_relevant_enabled",
|
||||
"temporary_locked",
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
OVEN_TEMPERATURE_ELECTROLUX_SENSORS: tuple[ElectroluxSensorDescription, ...] = (
|
||||
ElectroluxSensorDescription(
|
||||
key="food_probe_temperature",
|
||||
translation_key="food_probe_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda appliance, temp_unit=None: (
|
||||
appliance.get_current_display_food_probe_temperature_f()
|
||||
if temp_unit == UnitOfTemperature.FAHRENHEIT
|
||||
else appliance.get_current_display_food_probe_temperature_c()
|
||||
),
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(
|
||||
[DISPLAY_FOOD_PROBE_TEMPERATURE_F, DISPLAY_FOOD_PROBE_TEMPERATURE_C]
|
||||
),
|
||||
),
|
||||
ElectroluxSensorDescription(
|
||||
key="display_temperature",
|
||||
translation_key="display_temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda appliance, temp_unit=None: (
|
||||
appliance.get_current_display_temperature_f()
|
||||
if temp_unit == UnitOfTemperature.FAHRENHEIT
|
||||
else appliance.get_current_display_temperature_c()
|
||||
),
|
||||
exists_fn=lambda appliance: appliance.is_feature_supported(
|
||||
[DISPLAY_TEMPERATURE_C, DISPLAY_TEMPERATURE_F]
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def build_entities_for_appliance(
|
||||
appliance_data: ApplianceData,
|
||||
coordinators: dict[str, ElectroluxDataUpdateCoordinator],
|
||||
) -> list[ElectroluxBaseEntity]:
|
||||
"""Return all entities for a single appliance."""
|
||||
appliance = appliance_data.appliance
|
||||
coordinator = coordinators[appliance.applianceId]
|
||||
entities: list[ElectroluxBaseEntity] = []
|
||||
|
||||
if isinstance(appliance_data, OVAppliance):
|
||||
entities.extend(
|
||||
ElectroluxSensor(appliance_data, coordinator, description)
|
||||
for description in OVEN_ELECTROLUX_SENSORS
|
||||
if description.exists_fn(appliance_data)
|
||||
)
|
||||
|
||||
entities.extend(
|
||||
ElectroluxTemperatureSensor(appliance_data, coordinator, description)
|
||||
for description in OVEN_TEMPERATURE_ELECTROLUX_SENSORS
|
||||
if description.exists_fn(appliance_data)
|
||||
)
|
||||
|
||||
return entities
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ElectroluxConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set sensor for Electrolux Integration."""
|
||||
await async_setup_entities_helper(
|
||||
hass, entry, async_add_entities, build_entities_for_appliance
|
||||
)
|
||||
|
||||
|
||||
class ElectroluxSensor(ElectroluxBaseEntity[ApplianceData], SensorEntity):
|
||||
"""Representation of a generic sensor for Electrolux appliances."""
|
||||
|
||||
entity_description: ElectroluxSensorDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: ApplianceData,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
description: ElectroluxSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(appliance_data, coordinator, description.key)
|
||||
|
||||
if (
|
||||
description.feature_name is not None
|
||||
and description.known_values is not None
|
||||
):
|
||||
options = appliance_data.get_feature_state_string_options(
|
||||
description.feature_name
|
||||
)
|
||||
snake_case_options = [
|
||||
snake_case_option
|
||||
for option in options
|
||||
if (snake_case_option := _convert_to_snake_case(option))
|
||||
in description.known_values
|
||||
]
|
||||
|
||||
if len(snake_case_options) > 0:
|
||||
self._attr_options = snake_case_options
|
||||
|
||||
self.entity_description = description
|
||||
|
||||
def _update_attr_state(self) -> bool:
|
||||
new_value = self._get_value()
|
||||
if isinstance(new_value, str):
|
||||
new_value = _convert_to_snake_case(new_value)
|
||||
|
||||
if self.entity_description.known_values:
|
||||
new_value = _map_to_known_value(
|
||||
self.entity_description.known_values,
|
||||
self.entity_description.key,
|
||||
new_value,
|
||||
)
|
||||
|
||||
if self._attr_native_value != new_value:
|
||||
self._attr_native_value = new_value
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _get_value(self) -> StateType:
|
||||
return self.entity_description.value_fn(self._appliance_data)
|
||||
|
||||
|
||||
class ElectroluxTemperatureSensor(ElectroluxSensor):
|
||||
"""Representation of a temperature sensor for Electrolux appliances."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
appliance_data: ApplianceData,
|
||||
coordinator: ElectroluxDataUpdateCoordinator,
|
||||
description: ElectroluxSensorDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
self._appliance = cast(OVAppliance | CRAppliance, appliance_data)
|
||||
self._attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
super().__init__(appliance_data, coordinator, description)
|
||||
|
||||
def _get_value(self) -> StateType:
|
||||
temp_unit = self._get_temperature_unit()
|
||||
temp_value: float | None = cast(
|
||||
float | None,
|
||||
self.entity_description.value_fn(self._appliance_data, temp_unit=temp_unit),
|
||||
)
|
||||
if temp_value is None:
|
||||
return None
|
||||
return TemperatureConverter.convert(
|
||||
temp_value, temp_unit, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
def _get_temperature_unit(self) -> UnitOfTemperature:
|
||||
temp_unit = self._appliance.get_current_temperature_unit()
|
||||
|
||||
if temp_unit is not None:
|
||||
temp_unit = temp_unit.upper()
|
||||
|
||||
return ELECTROLUX_TO_HA_TEMPERATURE_UNIT.get(
|
||||
temp_unit, UnitOfTemperature.CELSIUS
|
||||
)
|
||||
|
||||
|
||||
def _convert_to_snake_case(x: str) -> str:
|
||||
"""Converts a string to snake case."""
|
||||
lower_case = x.lower()
|
||||
return "".join([_convert_char_to_snake_case(char) for char in lower_case])
|
||||
|
||||
|
||||
def _convert_char_to_snake_case(char: str) -> str:
|
||||
if char.isspace():
|
||||
return "_"
|
||||
return char
|
||||
|
||||
|
||||
def _map_to_known_value(
|
||||
known_values: set[str], entity_key: str, value: str
|
||||
) -> str | None:
|
||||
"""Return provided value if it is known, otherwise log warn message and return None."""
|
||||
if value not in known_values:
|
||||
_LOGGER.warning(
|
||||
"An unknown value %s was reported for a sensor of the Electrolux integration. "
|
||||
"Please report it for the integration, and include the following information: "
|
||||
'entity key="%s", reported value="%s"',
|
||||
value,
|
||||
entity_key,
|
||||
value,
|
||||
)
|
||||
return None
|
||||
return value
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This Electrolux account is already configured."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Unable to connect to the Electrolux API. Please check credentials and try again.",
|
||||
"invalid_auth": "Authentication failed. Please check your credentials."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"access_token": "[%key:common::config_flow::data::access_token%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"refresh_token": "Refresh token"
|
||||
},
|
||||
"data_description": {
|
||||
"access_token": "The access token from Electrolux Group for Developer.",
|
||||
"api_key": "Your Electrolux Group for Developer API key.",
|
||||
"refresh_token": "The refresh token used to renew your access token."
|
||||
},
|
||||
"description": "Please go to the [developer portal]({portal_link}) to generate new access and refresh tokens, then paste them below.",
|
||||
"title": "Configure your Electrolux Group account"
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"appliance_state": {
|
||||
"name": "Appliance state",
|
||||
"state": {
|
||||
"alarm": "Alarm",
|
||||
"delayed_start": "Delayed start",
|
||||
"end_of_cycle": "Cycle ended",
|
||||
"idle": "[%key:common::state::idle%]",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"paused": "[%key:common::state::paused%]",
|
||||
"ready_to_start": "Ready to start",
|
||||
"running": "Running"
|
||||
}
|
||||
},
|
||||
"display_temperature": {
|
||||
"name": "Current temperature"
|
||||
},
|
||||
"food_probe_state": {
|
||||
"name": "Food probe state",
|
||||
"state": {
|
||||
"inserted": "Inserted",
|
||||
"not_inserted": "Not inserted"
|
||||
}
|
||||
},
|
||||
"food_probe_temperature": {
|
||||
"name": "Food probe temperature"
|
||||
},
|
||||
"remote_control": {
|
||||
"name": "Remote control",
|
||||
"state": {
|
||||
"disabled": "[%key:common::state::disabled%]",
|
||||
"enabled": "[%key:common::state::enabled%]",
|
||||
"not_safety_relevant_enabled": "Not safety relevant enabled",
|
||||
"temporary_locked": "Temporarily locked"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,9 +150,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
|
||||
|
||||
raw_pcm_compatible = (
|
||||
metadata.codec == AudioCodecs.PCM
|
||||
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel is AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate is AudioBitRates.BITRATE_16
|
||||
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
|
||||
and metadata.channel == AudioChannels.CHANNEL_MONO
|
||||
and metadata.bit_rate == AudioBitRates.BITRATE_16
|
||||
)
|
||||
if raw_pcm_compatible:
|
||||
file_format = "pcm_s16le_16"
|
||||
|
||||
@@ -50,5 +50,5 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
# Zone in NORMAL state is OFF; any other state is ON
|
||||
self._attr_is_on = bool(
|
||||
self._element.logical_status is not ZoneLogicalStatus.NORMAL
|
||||
self._element.logical_status != ZoneLogicalStatus.NORMAL
|
||||
)
|
||||
|
||||
@@ -104,7 +104,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
ThermostatMode.EMERGENCY_HEAT,
|
||||
):
|
||||
return self._element.heat_setpoint
|
||||
if self._element.mode is ThermostatMode.COOL:
|
||||
if self._element.mode == ThermostatMode.COOL:
|
||||
return self._element.cool_setpoint
|
||||
return None
|
||||
|
||||
@@ -162,6 +162,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
|
||||
self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode]
|
||||
if (
|
||||
self._attr_hvac_mode == HVACMode.OFF
|
||||
and self._element.fan is ThermostatFan.ON
|
||||
and self._element.fan == ThermostatFan.ON
|
||||
):
|
||||
self._attr_hvac_mode = HVACMode.FAN_ONLY
|
||||
|
||||
@@ -56,7 +56,7 @@ class ElkNumberSetting(ElkAttachedEntity, NumberEntity):
|
||||
def __init__(self, element: Setting, elk: Any, elk_data: ELKM1Data) -> None:
|
||||
"""Initialize the number setting."""
|
||||
super().__init__(element, elk, elk_data)
|
||||
if element.value_format is SettingFormat.TIMER:
|
||||
if element.value_format == SettingFormat.TIMER:
|
||||
self._attr_device_class = NumberDeviceClass.DURATION
|
||||
self._attr_native_unit_of_measurement = UnitOfTime.SECONDS
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
for setting in elk.settings:
|
||||
setting = cast(Setting, setting)
|
||||
domain = (
|
||||
"time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
|
||||
"time" if setting.value_format == SettingFormat.TIME_OF_DAY else "number"
|
||||
)
|
||||
|
||||
orig_unique_id = generate_unique_id(elk_data.prefix, setting)
|
||||
@@ -288,7 +288,7 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def temperature_unit(self) -> str | None:
|
||||
"""Return the temperature unit."""
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
return None
|
||||
|
||||
@@ -305,18 +305,18 @@ class ElkZone(ElkSensor):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit of measurement."""
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
return self._temperature_unit
|
||||
if self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
if self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
return UnitOfElectricPotential.VOLT
|
||||
return None
|
||||
|
||||
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
|
||||
if self._element.definition is ZoneType.TEMPERATURE:
|
||||
if self._element.definition == ZoneType.TEMPERATURE:
|
||||
self._attr_native_value = temperature_to_state(
|
||||
self._element.temperature, UNDEFINED_TEMPERATURE
|
||||
)
|
||||
elif self._element.definition is ZoneType.ANALOG_ZONE:
|
||||
elif self._element.definition == ZoneType.ANALOG_ZONE:
|
||||
self._attr_native_value = f"{self._element.voltage}"
|
||||
else:
|
||||
self._attr_native_value = pretty_const(self._element.logical_status.name)
|
||||
|
||||
@@ -66,7 +66,7 @@ class ElkThermostatEMHeat(ElkEntity, SwitchEntity):
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Get the current emergency heat status."""
|
||||
return self._element.mode is ThermostatMode.EMERGENCY_HEAT
|
||||
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
|
||||
|
||||
def _elk_set(self, mode: ThermostatMode) -> None:
|
||||
"""Set the thermostat mode."""
|
||||
|
||||
@@ -30,7 +30,7 @@ async def async_setup_entry(
|
||||
time_settings = [
|
||||
setting
|
||||
for setting in cast(list[Setting], elk.settings)
|
||||
if setting.value_format is SettingFormat.TIME_OF_DAY
|
||||
if setting.value_format == SettingFormat.TIME_OF_DAY
|
||||
]
|
||||
|
||||
create_elk_entities(
|
||||
|
||||
@@ -96,7 +96,7 @@ def __get_coordinator(call: ServiceCall) -> EnergyZeroDataUpdateCoordinator:
|
||||
"config_entry": entry_id,
|
||||
},
|
||||
)
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="unloaded_config_entry",
|
||||
@@ -125,7 +125,7 @@ async def __get_prices(
|
||||
|
||||
data: Electricity | Gas
|
||||
|
||||
if price_type is PriceType.GAS:
|
||||
if price_type == PriceType.GAS:
|
||||
data = await coordinator.energyzero.get_gas_prices_legacy(
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"service": "mdi:refresh"
|
||||
},
|
||||
"set_dhw_override": {
|
||||
"service": "mdi:water-boiler"
|
||||
"service": "mdi:water-heater"
|
||||
},
|
||||
"set_system_mode": {
|
||||
"service": "mdi:pencil"
|
||||
|
||||
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
|
||||
|
||||
async def _async_finish_startup(hass: HomeAssistant) -> None:
|
||||
"""Run this only when HA has finished its startup."""
|
||||
if entry.state is ConfigEntryState.LOADED:
|
||||
if entry.state == ConfigEntryState.LOADED:
|
||||
await coordinator.async_refresh()
|
||||
else:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
@@ -16,7 +16,7 @@ class DeviceType(Enum):
|
||||
GAME_CONSOLE = "mdi:nintendo-game-boy"
|
||||
STREAMING_DONGLE = "mdi:cast"
|
||||
LOUDSPEAKER = SOUND_SYSTEM = STB = SATELLITE = MUSIC = "mdi:speaker"
|
||||
DISC_PLAYER = "mdi:disc-player"
|
||||
DISC_PLAYER = "mdi:disk-player"
|
||||
REMOTE_CONTROL = "mdi:remote-tv"
|
||||
RADIO = "mdi:radio"
|
||||
PHOTO_CAMERA = PHOTOS = "mdi:camera"
|
||||
|
||||
@@ -284,7 +284,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Manage initial options."""
|
||||
entry = self._get_entry()
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
self.client = entry.runtime_data
|
||||
|
||||
@@ -109,7 +109,7 @@ def setup_service(hass: HomeAssistant) -> None:
|
||||
entry: FlumeConfigEntry | None = hass.config_entries.async_get_entry(entry_id)
|
||||
if not entry:
|
||||
raise ValueError(f"Invalid config entry: {entry_id}")
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
if not entry.state == ConfigEntryState.LOADED:
|
||||
raise ValueError(f"Config entry not loaded: {entry_id}")
|
||||
return {
|
||||
"notifications": entry.runtime_data.notifications_coordinator.notifications # type: ignore[dict-item]
|
||||
|
||||
@@ -136,7 +136,7 @@ class FluxLedConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
ConfigEntryState.SETUP_IN_PROGRESS,
|
||||
ConfigEntryState.NOT_LOADED,
|
||||
)
|
||||
) or entry.state is ConfigEntryState.SETUP_RETRY:
|
||||
) or entry.state == ConfigEntryState.SETUP_RETRY:
|
||||
self.hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
else:
|
||||
async_dispatcher_send(
|
||||
|
||||
@@ -51,7 +51,7 @@ async def async_setup_entry(
|
||||
entry.data.get(CONF_NAME, entry.title)
|
||||
base_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
if device.device_type is DeviceType.Switch:
|
||||
if device.device_type == DeviceType.Switch:
|
||||
entities.append(FluxPowerStateSelect(coordinator.device, entry))
|
||||
if device.operating_modes:
|
||||
entities.append(
|
||||
|
||||
@@ -32,7 +32,7 @@ async def async_setup_entry(
|
||||
entities: list[FluxSwitch | FluxRemoteAccessSwitch | FluxMusicSwitch] = []
|
||||
base_unique_id = entry.unique_id or entry.entry_id
|
||||
|
||||
if coordinator.device.device_type is DeviceType.Switch:
|
||||
if coordinator.device.device_type == DeviceType.Switch:
|
||||
entities.append(FluxSwitch(coordinator, base_unique_id, None))
|
||||
|
||||
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
|
||||
|
||||
@@ -9,12 +9,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
EntityCategory,
|
||||
UnitOfDataRate,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
@@ -50,7 +45,6 @@ CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
translation_key="missed",
|
||||
native_unit_of_measurement="calls",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -59,7 +53,6 @@ DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
key="partition_free_space",
|
||||
translation_key="partition_free_space",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -87,7 +80,6 @@ async def async_setup_entry(
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
),
|
||||
)
|
||||
for sensor_id, sensor_name in router.sensors_temperature_names.items()
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import Any
|
||||
from freebox_api.exceptions import InsufficientPermissionsError
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -18,6 +19,7 @@ SWITCH_DESCRIPTIONS = [
|
||||
SwitchEntityDescription(
|
||||
key="wifi",
|
||||
translation_key="wifi",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ class FroniusSolarNet:
|
||||
inverter_info=_inverter_info,
|
||||
config_entry=self.config_entry,
|
||||
)
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
await _coordinator.async_refresh()
|
||||
else:
|
||||
await _coordinator.async_config_entry_first_refresh()
|
||||
@@ -220,7 +220,7 @@ class FroniusSolarNet:
|
||||
|
||||
# Only for re-scans. Initial setup adds entities
|
||||
# through sensor.async_setup_entry
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
async_dispatcher_send(self.hass, SOLAR_NET_DISCOVERY_NEW, _coordinator)
|
||||
|
||||
_LOGGER.debug(
|
||||
@@ -235,7 +235,7 @@ class FroniusSolarNet:
|
||||
try:
|
||||
_inverter_info = await self.fronius.inverter_info()
|
||||
except FroniusError as err:
|
||||
if self.config_entry.state is ConfigEntryState.LOADED:
|
||||
if self.config_entry.state == ConfigEntryState.LOADED:
|
||||
# During a re-scan we will attempt again as per schedule.
|
||||
_LOGGER.debug("Re-scan failed for %s", self.host)
|
||||
return inverter_infos
|
||||
|
||||
@@ -42,7 +42,7 @@ async def _collect_coordinators(
|
||||
raise HomeAssistantError(f"Device '{target}' not found in device registry")
|
||||
coordinators = list[FullyKioskDataUpdateCoordinator]()
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state is not ConfigEntryState.LOADED:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
coordinators.append(config_entry.runtime_data)
|
||||
return coordinators
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"entity": {
|
||||
"button": {
|
||||
"sync_clock": {
|
||||
"default": "mdi:clock-check"
|
||||
"default": "mdi:clock-sync"
|
||||
}
|
||||
},
|
||||
"number": {
|
||||
|
||||
@@ -75,7 +75,7 @@ async def async_setup_entry(
|
||||
|
||||
mfg_data = await async_get_manufacturer_data({address})
|
||||
product_type = mfg_data[address].product_type
|
||||
if product_type is ProductType.UNKNOWN:
|
||||
if product_type == ProductType.UNKNOWN:
|
||||
raise ConfigEntryNotReady("Unable to find product type")
|
||||
|
||||
client = Client(get_connection(hass, address), product_type)
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioghost"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["aioghost==0.4.16"]
|
||||
"requirements": ["aioghost==0.4.0"]
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ def _get_entity_descriptions(
|
||||
local_sync = True
|
||||
if (
|
||||
search := data.get(CONF_SEARCH)
|
||||
) or calendar_item.access_role is AccessRole.FREE_BUSY_READER:
|
||||
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
|
||||
read_only = True
|
||||
local_sync = False
|
||||
entity_description = GoogleCalendarEntityDescription(
|
||||
@@ -386,14 +386,14 @@ class GoogleCalendarEntity(
|
||||
"""Return True if the event is visible and not declined."""
|
||||
|
||||
if any(
|
||||
attendee.is_self and attendee.response_status is ResponseStatus.DECLINED
|
||||
attendee.is_self and attendee.response_status == ResponseStatus.DECLINED
|
||||
for attendee in event.attendees
|
||||
):
|
||||
return False
|
||||
# Calendar enttiy may be limited to a specific event type
|
||||
if (
|
||||
self.entity_description.event_type is not None
|
||||
and self.entity_description.event_type is not event.event_type
|
||||
and self.entity_description.event_type != event.event_type
|
||||
):
|
||||
return False
|
||||
# Default calendar entity omits the special types but includes all the others
|
||||
|
||||
@@ -247,7 +247,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the location step."""
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -225,7 +225,7 @@ class LLMSubentryFlowHandler(ConfigSubentryFlow):
|
||||
) -> SubentryFlowResult:
|
||||
"""Set conversation options."""
|
||||
# abort if entry is not loaded
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -754,7 +754,7 @@ async def async_prepare_files_for_prompt(
|
||||
config={"http_options": {"timeout": TIMEOUT_MILLIS}},
|
||||
)
|
||||
|
||||
if uploaded_file.state is FileState.FAILED:
|
||||
if uploaded_file.state == FileState.FAILED:
|
||||
raise HomeAssistantError(
|
||||
f"File `{uploaded_file.name}` processing"
|
||||
" failed, reason:"
|
||||
@@ -766,7 +766,7 @@ async def async_prepare_files_for_prompt(
|
||||
tasks = [
|
||||
asyncio.create_task(wait_for_file_processing(part))
|
||||
for part in prompt_parts
|
||||
if part.state is not FileState.ACTIVE
|
||||
if part.state != FileState.ACTIVE
|
||||
]
|
||||
async with asyncio.timeout(TIMEOUT_MILLIS / 1000):
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
@@ -237,7 +237,7 @@ class LocationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> SubentryFlowResult:
|
||||
"""Handle the location step."""
|
||||
if self._get_entry().state is not ConfigEntryState.LOADED:
|
||||
if self._get_entry().state != ConfigEntryState.LOADED:
|
||||
return self.async_abort(reason="entry_not_loaded")
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -26,7 +26,7 @@ def _get_coordinators(
|
||||
coordinators: dict[str, GrowattCoordinator] = {}
|
||||
|
||||
for entry in hass.config_entries.async_entries(DOMAIN):
|
||||
if entry.state is not ConfigEntryState.LOADED:
|
||||
if entry.state != ConfigEntryState.LOADED:
|
||||
continue
|
||||
|
||||
for coord in entry.runtime_data.devices.values():
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["guntamatic==1.9.0"]
|
||||
"requirements": ["guntamatic==1.8.0"]
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class SupervisorOSUpdateEntity(HassioOSEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/operating-system/commits/dev"
|
||||
return (
|
||||
f"https://github.com/home-assistant/operating-system/releases/tag/{version}"
|
||||
@@ -304,7 +304,7 @@ class SupervisorSupervisorUpdateEntity(HassioSupervisorEntity, UpdateEntity):
|
||||
def release_url(self) -> str | None:
|
||||
"""URL to the full release notes of the latest version available."""
|
||||
version = AwesomeVersion(self.latest_version)
|
||||
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
|
||||
if version.dev or version.strategy == AwesomeVersionStrategy.UNKNOWN:
|
||||
return "https://github.com/home-assistant/supervisor/commits/main"
|
||||
return f"https://github.com/home-assistant/supervisor/releases/tag/{version}"
|
||||
|
||||
|
||||
@@ -150,7 +150,7 @@ def _get_controller(hass: HomeAssistant) -> Heos:
|
||||
hass.config_entries.async_entry_for_domain_unique_id(DOMAIN, DOMAIN)
|
||||
)
|
||||
|
||||
if not entry or entry.state is not ConfigEntryState.LOADED:
|
||||
if not entry or not entry.state == ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN, translation_key="integration_not_loaded"
|
||||
)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
# pylint: disable-next=home-assistant-duplicate-const
|
||||
ATTR_MODE = "mode"
|
||||
ATTR_TIME_PERIOD = "time_period"
|
||||
ATTR_ONOFF = "on_off"
|
||||
CONF_CODE = "2fa"
|
||||
|
||||
@@ -12,12 +12,12 @@ from homeassistant.components.light import (
|
||||
ColorMode,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.const import ATTR_MODE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util import color as color_util
|
||||
|
||||
from . import HiveConfigEntry, refresh_system
|
||||
from .const import ATTR_MODE
|
||||
from .entity import HiveEntity
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user