Compare commits

..

29 Commits

Author SHA1 Message Date
J. Nick Koston 974b5ece19 Verify state_changed unsubscribe is invoked on overflow
Add a spy on async_track_state_change_event so the test directly
asserts the live stream's unsubscribe callable is invoked when the
queue overflows, instead of relying on the (unreliable) shared
EVENT_STATE_CHANGED bus listener count.
2026-05-21 14:21:35 -05:00
J. Nick Koston 47d21a7204 Fix flaky test_overflow_queue in history websocket tests
Comparing the full bus listener dict can fail when unrelated components
asynchronously add or remove EVENT_STATE_CHANGED listeners during the
test; capture listeners before and after via a context manager that
excludes state_changed.
2026-05-21 14:08:03 -05:00
Lukas e3dd6b5fc5 Fix hardcoded exception strings in pooldose integration (#171652)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 20:36:17 +02:00
Ariel Ebersberger 94d620438b Use is/is not for same-enum identity comparisons (source) (#171591) 2026-05-21 19:30:55 +02:00
Erik Montnemery 8867b792dc Remove use of advanced mode from the zha integration (#171753) 2026-05-21 19:26:24 +02:00
G Johansson 97967abfeb Fix missing string in smhi (#171756) 2026-05-21 19:25:03 +02:00
mhuiskes af8fea272d Declare Bronze quality scale for Zeversolar integration (#170410) 2026-05-21 19:12:54 +02:00
Simone Chemelli 2db0eed570 Fix hardcoded exception strings in samsungtv (#171745) 2026-05-21 18:59:37 +02:00
Erwin Douna ded1628c20 Downloader add missing data description (#171727) 2026-05-21 18:59:24 +02:00
Petro31 a02e54f332 Update documentation link to point to each domain/platform (#171734)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 18:47:39 +02:00
Erik Montnemery 1858649bc7 Improve tests of trigger variables (#171742) 2026-05-21 17:55:41 +02:00
Leonardo Merza 109e09c3ec Add fan minimum on time number entity to ecobee (#171419)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 17:53:02 +02:00
Manu ad139b259b Add notify entity to System Bridge integration (#171736)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-21 17:49:25 +02:00
Scott Giminiani a1a76874fd Fix name of config flow form field (#171741) 2026-05-21 17:33:53 +02:00
Ariel Ebersberger e7bd56325b Use is for IntentResponseType identity check in conversation (#171699)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: arturpragacz <49985303+arturpragacz@users.noreply.github.com>
2026-05-21 17:31:14 +02:00
J. Nick Koston ef2ef0c8ba Bump zeroconf to 0.149.16 (#171737) 2026-05-21 17:28:26 +02:00
Max Michels a8381e923a Replace duplicate constants with homeassistant.const imports (#171675) 2026-05-21 17:13:31 +02:00
Max Michels b7adba559b Replace duplicate constants with homeassistant.const imports (#171677) 2026-05-21 17:13:20 +02:00
Matthias Alphart 5cf6dceb04 Normalize empty string to None in knx config flow (#171693) 2026-05-21 17:13:11 +02:00
Paul Bottein 975bcc5431 Reorganize Freebox entity categories (#171480) 2026-05-21 16:55:27 +02:00
Michael Barrett f24a44e81f Update aioghost to 0.4.16 (#171690) 2026-05-21 16:53:45 +02:00
Phil-Rad 43c91843cd Remove unreachable import config flow path from cert_expiry (#171733) 2026-05-21 16:50:49 +02:00
Chris dbce1d328a Bump python-openevse-http to 0.3.4 (#171621) 2026-05-21 16:46:54 +02:00
Petro31 d294b04b79 Add EntityComponent to device_tracker (#171507) 2026-05-21 16:10:20 +02:00
Markus Tuominen 8b0e9060b3 Set _attr_has_entity_name on tplink_omada OmadaClientScannerEntity (#171680) 2026-05-21 16:41:37 +03:00
MoonDevLT 39066b6e3a Fix missing exceptions translation key missing_device_info in lunatone (#171569) 2026-05-21 14:59:48 +02:00
Max Michels a23a9b350b Replace duplicate constants with homeassistant.const imports (#171701) 2026-05-21 14:57:58 +02:00
chiro79 fdaa807ca8 Switch to aiopvpc-ng (#171025) 2026-05-21 14:54:23 +02:00
A. Gideonse f290dcc03f Update Indevolt integration quality scale to platinum (#170320) 2026-05-21 14:53:06 +02:00
250 changed files with 1777 additions and 4932 deletions
Generated
+2 -4
View File
@@ -466,8 +466,6 @@ 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
@@ -1415,8 +1413,8 @@ CLAUDE.md @home-assistant/core
/tests/components/pushover/ @engrbm87
/homeassistant/components/pvoutput/ @frenck
/tests/components/pvoutput/ @frenck
/homeassistant/components/pvpc_hourly_pricing/ @azogue
/tests/components/pvpc_hourly_pricing/ @azogue
/homeassistant/components/pvpc_hourly_pricing/ @azogue @chiro79
/tests/components/pvpc_hourly_pricing/ @azogue @chiro79
/homeassistant/components/pyload/ @tr4nt0r
/tests/components/pyload/ @tr4nt0r
/homeassistant/components/qbittorrent/ @geoffreylagaisse @finder39
+1 -1
View File
@@ -134,7 +134,7 @@ class AuthManagerFlowManager(
"""
flow = cast(LoginFlow, flow)
if result["type"] != FlowResultType.CREATE_ENTRY:
if result["type"] is not FlowResultType.CREATE_ENTRY:
return result
# we got final result
@@ -226,7 +226,7 @@ class ConversationSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Set initial options."""
# abort if entry is not loaded
if self._get_entry().state != ConfigEntryState.LOADED:
if self._get_entry().state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
hass_apis: list[SelectOptionDict] = [
@@ -89,7 +89,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
def supported_features(self) -> WaterHeaterEntityFeature:
"""Return the list of supported features."""
supports_vacation_mode = any(
supported_mode.mode == AOSmithOperationMode.VACATION
supported_mode.mode is AOSmithOperationMode.VACATION
for supported_mode in self.device.supported_modes
)
@@ -122,7 +122,7 @@ class AOSmithWaterHeaterEntity(AOSmithStatusEntity, WaterHeaterEntity):
@property
def is_away_mode_on(self) -> bool:
"""Return True if away mode is on."""
return self.device.status.current_mode == AOSmithOperationMode.VACATION
return self.device.status.current_mode is AOSmithOperationMode.VACATION
async def async_set_operation_mode(self, operation_mode: str) -> None:
"""Set new target operation mode."""
@@ -369,7 +369,7 @@ class AppleTVManager(DeviceListener):
attrs[ATTR_MODEL] = (
dev_info.raw_model
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
else model_str(dev_info.model)
)
attrs[ATTR_SW_VERSION] = dev_info.version
@@ -63,7 +63,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
# Listen to keyboard updates
atv.keyboard.listener = self
# Set initial state based on current focus state
self._update_state(atv.keyboard.text_focus_state == KeyboardFocusState.Focused)
self._update_state(atv.keyboard.text_focus_state is KeyboardFocusState.Focused)
@callback
def async_device_disconnected(self) -> None:
@@ -78,7 +78,7 @@ class AppleTVKeyboardFocused(AppleTVEntity, BinarySensorEntity, KeyboardListener
This is a callback function from pyatv.interface.KeyboardListener.
"""
self._update_state(new_state == KeyboardFocusState.Focused)
self._update_state(new_state is KeyboardFocusState.Focused)
def _update_state(self, new_state: bool) -> None:
"""Update and report."""
@@ -354,7 +354,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
"name": self.atv.name,
"type": (
dev_info.raw_model
if dev_info.model == DeviceModel.Unknown and dev_info.raw_model
if dev_info.model is DeviceModel.Unknown and dev_info.raw_model
else model_str(dev_info.model)
),
}
@@ -441,12 +441,12 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
return await self.async_step_password()
# Figure out, depending on protocol, what kind of pairing is needed
if service.pairing == PairingRequirement.Unsupported:
if service.pairing is PairingRequirement.Unsupported:
_LOGGER.debug("%s does not support pairing", self.protocol)
return await self.async_pair_next_protocol()
if service.pairing == PairingRequirement.Disabled:
if service.pairing is PairingRequirement.Disabled:
return await self.async_step_protocol_disabled()
if service.pairing == PairingRequirement.NotNeeded:
if service.pairing is PairingRequirement.NotNeeded:
_LOGGER.debug("%s does not require pairing", self.protocol)
self.credentials[self.protocol.value] = None
return await self.async_pair_next_protocol()
@@ -457,7 +457,7 @@ class AppleTVConfigFlow(ConfigFlow, domain=DOMAIN):
pair_args: dict[str, Any] = {}
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
pair_args["name"] = "Home Assistant"
if self.protocol == Protocol.DMAP:
if self.protocol is Protocol.DMAP:
pair_args["zeroconf"] = await zeroconf.async_get_instance(self.hass)
# Initiate the pairing process
@@ -139,7 +139,7 @@ class AppleTvMediaPlayer(
all_features = atv.features.all_features()
for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items():
feature_info = all_features.get(feature_name)
if feature_info and feature_info.state != FeatureState.Unsupported:
if feature_info and feature_info.state is not FeatureState.Unsupported:
self._attr_supported_features |= support_flag
# No need to schedule state update here as that will happen when the first
@@ -188,14 +188,14 @@ class AppleTvMediaPlayer(
return MediaPlayerState.OFF
if (
self._is_feature_available(FeatureName.PowerState)
and self.atv.power.power_state == PowerState.Off
and self.atv.power.power_state is PowerState.Off
):
return MediaPlayerState.OFF
if self._playing:
state = self._playing.device_state
if state in (DeviceState.Idle, DeviceState.Loading):
return MediaPlayerState.IDLE
if state == DeviceState.Playing:
if state is DeviceState.Playing:
return MediaPlayerState.PLAYING
if state in (DeviceState.Paused, DeviceState.Seeking, DeviceState.Stopped):
return MediaPlayerState.PAUSED
@@ -446,7 +446,7 @@ class AppleTvMediaPlayer(
def shuffle(self) -> bool | None:
"""Boolean if shuffle is enabled."""
if self._playing and self._is_feature_available(FeatureName.Shuffle):
return self._playing.shuffle != ShuffleState.Off
return self._playing.shuffle is not ShuffleState.Off
return None
def _is_feature_available(self, feature: FeatureName) -> bool:
@@ -506,7 +506,7 @@ class AppleTvMediaPlayer(
and (self._is_feature_available(FeatureName.TurnOff))
and (
not self._is_feature_available(FeatureName.PowerState)
or self.atv.power.power_state == PowerState.On
or self.atv.power.power_state is PowerState.On
)
):
await self.atv.power.turn_off()
@@ -59,7 +59,7 @@ def _check_keyboard_focus(atv: AppleTVInterface) -> None:
translation_domain=DOMAIN,
translation_key="keyboard_not_available",
) from err
if focus_state != KeyboardFocusState.Focused:
if focus_state is not KeyboardFocusState.Focused:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="keyboard_not_focused",
@@ -263,9 +263,9 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
def media_channel(self) -> str | None:
"""Channel currently playing."""
source = self._state.get_source()
if source == SourceCodes.DAB:
if source is SourceCodes.DAB:
value = self._state.get_dab_station()
elif source == SourceCodes.FM:
elif source is SourceCodes.FM:
value = self._state.get_rds_information()
else:
value = None
@@ -274,7 +274,7 @@ class ArcamFmj(ArcamFmjEntity, MediaPlayerEntity):
@property
def media_artist(self) -> str | None:
"""Artist of current playing media, music track only."""
if self._state.get_source() == SourceCodes.DAB:
if self._state.get_source() is SourceCodes.DAB:
value = self._state.get_dls_pdt()
else:
value = None
@@ -1355,7 +1355,7 @@ class PipelineRun:
) -> bool:
"""Return true if all targeted entities were in the same area as the device."""
if (
intent_response.response_type != intent.IntentResponseType.ACTION_DONE
intent_response.response_type is not intent.IntentResponseType.ACTION_DONE
or not intent_response.matched_states
):
return False
+4 -4
View File
@@ -251,12 +251,12 @@ class AuthProvidersView(HomeAssistantView):
def _prepare_result_json(result: AuthFlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
return {
key: val for key, val in result.items() if key not in ("result", "data")
}
if result["type"] != data_entry_flow.FlowResultType.FORM:
if result["type"] is not data_entry_flow.FlowResultType.FORM:
return result # type: ignore[return-value]
data = dict(result)
@@ -289,11 +289,11 @@ class LoginFlowBaseView(HomeAssistantView):
result: AuthFlowResult,
) -> web.Response:
"""Convert the flow result to a response."""
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
# @log_invalid_auth does not work here since it returns HTTP 200.
# We need to manually log failed login attempts.
if (
result["type"] == data_entry_flow.FlowResultType.FORM
result["type"] is data_entry_flow.FlowResultType.FORM
and (errors := result.get("errors"))
and errors.get("base")
in (
@@ -142,9 +142,9 @@ def websocket_depose_mfa(
def _prepare_result_json(result: data_entry_flow.FlowResult) -> dict[str, Any]:
"""Convert result to JSON serializable dict."""
if result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is data_entry_flow.FlowResultType.CREATE_ENTRY:
return dict(result)
if result["type"] != data_entry_flow.FlowResultType.FORM:
if result["type"] is not data_entry_flow.FlowResultType.FORM:
return result # type: ignore[return-value]
data = dict(result)
@@ -273,7 +273,7 @@ async def ws_subscribe_scanner_details(
def _async_registration_changed(registration: HaScannerRegistration) -> None:
added_event = HaScannerRegistrationEvent.ADDED
event_type = "add" if registration.event == added_event else "remove"
event_type = "add" if registration.event is added_event else "remove"
_async_event_message({event_type: [registration.scanner.details]})
manager = _get_manager(hass)
+4 -1
View File
@@ -158,7 +158,10 @@ def process_service_info(
)
# If payload is encrypted and the bindkey is not verified then we need to reauth
if data.encryption_scheme != EncryptionScheme.NONE and not data.bindkey_verified:
if (
data.encryption_scheme is not EncryptionScheme.NONE
and not data.bindkey_verified
):
entry.async_start_reauth(hass, data={"device": data})
return update
@@ -59,7 +59,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = discovery_info
self._discovered_device = device
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
return await self.async_step_bluetooth_confirm()
@@ -125,7 +125,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = discovery.discovery_info
self._discovered_device = discovery.device
if discovery.device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if discovery.device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
return self._async_get_or_create_entry()
@@ -164,7 +164,7 @@ class BTHomeConfigFlow(ConfigFlow, domain=DOMAIN):
self._discovery_info = device.last_service_info
if device.encryption_scheme == EncryptionScheme.BTHOME_BINDKEY:
if device.encryption_scheme is EncryptionScheme.BTHOME_BINDKEY:
return await self.async_step_get_encryption_key()
# Otherwise there wasn't actually encryption so abort
@@ -1,12 +1,11 @@
"""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 SOURCE_IMPORT, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_PORT
from .const import DEFAULT_PORT, DOMAIN
@@ -19,8 +18,6 @@ from .errors import (
)
from .helper import get_cert_expiry_timestamp
_LOGGER = logging.getLogger(__name__)
class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -75,9 +72,6 @@ class CertexpiryConfigFlow(ConfigFlow, domain=DOMAIN):
title=title,
data={CONF_HOST: host, CONF_PORT: port},
)
if self.source == SOURCE_IMPORT:
_LOGGER.error("Config import failed for %s", user_input[CONF_HOST])
return self.async_abort(reason="import_failed")
else:
user_input = {}
user_input[CONF_HOST] = ""
@@ -2,7 +2,6 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
"import_failed": "Import from config failed",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
@@ -110,7 +110,7 @@ class ComelitAlarmEntity(
@property
def available(self) -> bool:
"""Return True if alarm is available."""
if self._area.human_status == AlarmAreaState.UNKNOWN:
if self._area.human_status is AlarmAreaState.UNKNOWN:
return False
return super().available
@@ -124,7 +124,7 @@ class ComelitAlarmEntity(
self._area.human_status,
self._area.armed,
)
if self._area.human_status == AlarmAreaState.ARMED:
if self._area.human_status is AlarmAreaState.ARMED:
if self._area.armed == ALARM_AREA_ARMED_STATUS[AWAY]:
return AlarmControlPanelState.ARMED_AWAY
if self._area.armed == ALARM_AREA_ARMED_STATUS[NIGHT]:
@@ -43,7 +43,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
device_class=BinarySensorDeviceClass.PROBLEM,
is_on_fn=lambda obj: cast(ComelitVedoAreaObject, obj).anomaly,
available_fn=lambda obj: (
cast(ComelitVedoAreaObject, obj).human_status != AlarmAreaState.UNKNOWN
cast(ComelitVedoAreaObject, obj).human_status is not AlarmAreaState.UNKNOWN
),
),
ComelitBinarySensorEntityDescription(
@@ -67,7 +67,7 @@ BINARY_SENSOR_TYPES: Final[tuple[ComelitBinarySensorEntityDescription, ...]] = (
object_type=ALARM_ZONE,
device_class=BinarySensorDeviceClass.PROBLEM,
is_on_fn=lambda obj: (
cast(ComelitVedoZoneObject, obj).human_status == AlarmZoneState.FAULTY
cast(ComelitVedoZoneObject, obj).human_status is AlarmZoneState.FAULTY
),
available_fn=lambda obj: (
cast(ComelitVedoZoneObject, obj).human_status
+2 -2
View File
@@ -166,12 +166,12 @@ class ComelitVedoSensorEntity(
@property
def available(self) -> bool:
"""Sensor availability."""
return self._zone_object.human_status != AlarmZoneState.UNAVAILABLE
return self._zone_object.human_status is not AlarmZoneState.UNAVAILABLE
@property
def native_value(self) -> StateType:
"""Sensor value."""
if (status := self._zone_object.human_status) == AlarmZoneState.UNKNOWN:
if (status := self._zone_object.human_status) is AlarmZoneState.UNKNOWN:
return None
return cast(str, status.value)
@@ -148,7 +148,7 @@ def _prepare_config_flow_result_json(
prepare_result_json: Callable[[data_entry_flow.FlowResult], dict[str, Any]],
) -> dict[str, Any]:
"""Convert result to JSON."""
if result["type"] != data_entry_flow.FlowResultType.CREATE_ENTRY:
if result["type"] is not data_entry_flow.FlowResultType.CREATE_ENTRY:
return prepare_result_json(result)
data = {key: val for key, val in result.items() if key not in ("data", "context")}
@@ -646,7 +646,7 @@ class DefaultAgent(ConversationEntity):
cache_value = self._intent_cache.get(cache_key)
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.EXPOSED_ENTITIES_ONLY
cache_value.stage is IntentMatchingStage.EXPOSED_ENTITIES_ONLY
):
_LOGGER.debug("Got cached result for exposed entities")
return cache_value.result
@@ -686,7 +686,7 @@ class DefaultAgent(ConversationEntity):
skip_unexposed_entities_match = False
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.UNEXPOSED_ENTITIES
cache_value.stage is IntentMatchingStage.UNEXPOSED_ENTITIES
):
_LOGGER.debug("Got cached result for all entities")
return cache_value.result
@@ -731,7 +731,7 @@ class DefaultAgent(ConversationEntity):
skip_unknown_names = False
if cache_value is not None:
if (cache_value.result is not None) and (
cache_value.stage == IntentMatchingStage.UNKNOWN_NAMES
cache_value.stage is IntentMatchingStage.UNKNOWN_NAMES
):
_LOGGER.debug("Got cached result for unknown names")
return cache_value.result
@@ -1447,7 +1447,7 @@ class DefaultAgent(ConversationEntity):
response = await self._async_process_intent_result(result, user_input, chat_log)
if (
response.response_type == intent.IntentResponseType.ERROR
response.response_type is 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 == intent.MatchFailedReason.DOMAIN) and constraints.domains:
if (reason is intent.MatchFailedReason.DOMAIN) and constraints.domains:
domain = next(iter(constraints.domains)) # first domain
if constraints.area_name:
# domain in area
@@ -1565,7 +1565,7 @@ def _get_match_error_response(
# domain only
return ErrorKey.NO_DOMAIN, {"domain": domain}
if reason == intent.MatchFailedReason.DUPLICATE_NAME:
if reason is intent.MatchFailedReason.DUPLICATE_NAME:
if constraints.floor_name:
# duplicate on floor
return ErrorKey.DUPLICATE_ENTITIES_IN_FLOOR, {
@@ -1582,26 +1582,26 @@ def _get_match_error_response(
return ErrorKey.DUPLICATE_ENTITIES, {"entity": result.no_match_name}
if reason == intent.MatchFailedReason.INVALID_AREA:
if reason is intent.MatchFailedReason.INVALID_AREA:
# Invalid area name
return ErrorKey.NO_AREA, {"area": result.no_match_name}
if reason == intent.MatchFailedReason.INVALID_FLOOR:
if reason is intent.MatchFailedReason.INVALID_FLOOR:
# Invalid floor name
return ErrorKey.NO_FLOOR, {"floor": result.no_match_name}
if reason == intent.MatchFailedReason.FEATURE:
if reason is intent.MatchFailedReason.FEATURE:
# Feature not supported by entity
return ErrorKey.FEATURE_NOT_SUPPORTED, {}
if reason == intent.MatchFailedReason.STATE:
if reason is intent.MatchFailedReason.STATE:
# Entity is not in correct state
assert constraints.states
state = next(iter(constraints.states))
return ErrorKey.ENTITY_WRONG_STATE, {"state": state}
if reason == intent.MatchFailedReason.ASSISTANT:
if reason is intent.MatchFailedReason.ASSISTANT:
# Not exposed
if constraints.name:
if constraints.area_name:
@@ -80,7 +80,7 @@ async def async_validate_device_automation_config(
# the checks below which look for a config entry matching the device automation
# domain
if (
automation_type == DeviceAutomationType.ACTION
automation_type is DeviceAutomationType.ACTION
and validated_config[CONF_DOMAIN] in ENTITY_PLATFORMS
):
# Pass the unvalidated config to avoid mutating the raw config twice
@@ -1,11 +1,18 @@
"""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,
@@ -33,6 +40,8 @@ from .const import ( # noqa: F401
DEFAULT_TRACK_NEW,
DOMAIN,
ENTITY_ID_FORMAT,
LOGGER,
PLATFORM_TYPE_LEGACY,
SCAN_INTERVAL,
SourceType,
)
@@ -44,7 +53,9 @@ from .legacy import ( # noqa: F401
SOURCE_TYPES,
AsyncSeeCallback,
DeviceScanner,
DeviceTracker,
SeeCallback,
async_create_platform_type,
async_setup_integration as async_setup_legacy_integration,
see,
)
@@ -57,5 +68,43 @@ def is_on(hass: HomeAssistant, entity_id: str) -> bool:
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the device tracker."""
async_setup_legacy_integration(hass, config)
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,
)
return True
@@ -37,11 +37,7 @@ 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,
discovery,
entity_registry as er,
)
from homeassistant.helpers import config_validation as cv, entity_registry as er
from homeassistant.helpers.event import (
async_track_time_interval,
async_track_utc_time_change,
@@ -204,40 +200,7 @@ def see(
hass.services.call(DOMAIN, SERVICE_SEE, data)
@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(
async def async_setup_integration(
hass: HomeAssistant,
config: ConfigType,
tracker_future: asyncio.Future[DeviceTracker],
@@ -261,7 +261,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
except KeyError, ValueError:
bootid = None
if change == ssdp.SsdpChange.UPDATE:
if change is ssdp.SsdpChange.UPDATE:
# This is an announcement that bootid is about to change
if self._bootid is not None and self._bootid == bootid:
# Store the new value (because our old value matches) so that we
@@ -281,7 +281,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
await self._device_disconnect()
self._bootid = bootid
if change == ssdp.SsdpChange.BYEBYE:
if change is ssdp.SsdpChange.BYEBYE:
# Device is going away
if self._device:
# Disconnect from gone device
@@ -290,7 +290,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
self._ssdp_connect_failed = False
if (
change == ssdp.SsdpChange.ALIVE
change is ssdp.SsdpChange.ALIVE
and not self._device
and not self._ssdp_connect_failed
):
@@ -718,7 +718,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
# If already playing, or don't want to autoplay, no need to call Play
autoplay = extra.get("autoplay", True)
if self._device.transport_state == TransportState.PLAYING or not autoplay:
if self._device.transport_state is TransportState.PLAYING or not autoplay:
return
# Play it
@@ -748,7 +748,7 @@ class DlnaDmrEntity(MediaPlayerEntity):
if not (play_mode := self._device.play_mode):
return None
if play_mode == PlayMode.VENDOR_DEFINED:
if play_mode is PlayMode.VENDOR_DEFINED:
return None
return play_mode in (PlayMode.SHUFFLE, PlayMode.RANDOM)
@@ -782,10 +782,10 @@ class DlnaDmrEntity(MediaPlayerEntity):
if not (play_mode := self._device.play_mode):
return None
if play_mode == PlayMode.VENDOR_DEFINED:
if play_mode is PlayMode.VENDOR_DEFINED:
return None
if play_mode == PlayMode.REPEAT_ONE:
if play_mode is PlayMode.REPEAT_ONE:
return RepeatMode.ONE
if play_mode in (PlayMode.REPEAT_ALL, PlayMode.RANDOM):
+3 -3
View File
@@ -236,7 +236,7 @@ class DmsDeviceSource:
except KeyError, ValueError:
bootid = None
if change == ssdp.SsdpChange.UPDATE:
if change is ssdp.SsdpChange.UPDATE:
# This is an announcement that bootid is about to change
if self._bootid is not None and self._bootid == bootid:
# Store the new value (because our old value matches) so that we
@@ -258,7 +258,7 @@ class DmsDeviceSource:
await self.device_disconnect()
self._bootid = bootid
if change == ssdp.SsdpChange.BYEBYE:
if change is ssdp.SsdpChange.BYEBYE:
# Device is going away
if self._device:
# Disconnect from gone device
@@ -267,7 +267,7 @@ class DmsDeviceSource:
self._ssdp_connect_failed = False
if (
change == ssdp.SsdpChange.ALIVE
change is ssdp.SsdpChange.ALIVE
and not self._device
and not self._ssdp_connect_failed
):
@@ -8,6 +8,12 @@
},
"step": {
"user": {
"data": {
"download_dir": "Download directory"
},
"data_description": {
"download_dir": "The directory where downloaded files will be stored. This can be an absolute path or a path relative to the Home Assistant configuration directory."
},
"description": "Select a location to get to store downloads. The setup will check if the directory exists."
}
}
@@ -15,6 +15,8 @@ _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,4 +1,11 @@
{
"entity": {
"number": {
"fan_min_on_time": {
"default": "mdi:fan-clock"
}
}
},
"services": {
"create_vacation": {
"service": "mdi:umbrella-beach"
@@ -24,7 +24,6 @@ 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."""
+40 -2
View File
@@ -74,6 +74,10 @@ async def async_setup_entry(
)
)
entities.extend(
EcobeeFanMinOnTime(data, index) for index in range(len(data.ecobee.thermostats))
)
async_add_entities(entities, True)
@@ -86,7 +90,6 @@ 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,
@@ -130,7 +133,6 @@ 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
@@ -165,3 +167,39 @@ 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,6 +37,9 @@
"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,7 +53,6 @@ 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__(
@@ -104,7 +103,6 @@ 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__(
@@ -1,220 +0,0 @@
"""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
]
@@ -1,99 +0,0 @@
"""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
@@ -1,11 +0,0 @@
"""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}"
@@ -1,96 +0,0 @@
"""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)
@@ -1,80 +0,0 @@
"""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()
@@ -1,49 +0,0 @@
"""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
)
)
@@ -1,18 +0,0 @@
{
"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"
}
}
}
}
@@ -1,11 +0,0 @@
{
"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"]
}
@@ -1,70 +0,0 @@
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
@@ -1,290 +0,0 @@
"""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
@@ -1,66 +0,0 @@
{
"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"
}
}
}
}
}
+3 -3
View File
@@ -150,9 +150,9 @@ class ElevenLabsSTTEntity(SpeechToTextEntity):
raw_pcm_compatible = (
metadata.codec == AudioCodecs.PCM
and metadata.sample_rate == AudioSampleRates.SAMPLERATE_16000
and metadata.channel == AudioChannels.CHANNEL_MONO
and metadata.bit_rate == AudioBitRates.BITRATE_16
and metadata.sample_rate is AudioSampleRates.SAMPLERATE_16000
and metadata.channel is AudioChannels.CHANNEL_MONO
and metadata.bit_rate is AudioBitRates.BITRATE_16
)
if raw_pcm_compatible:
file_format = "pcm_s16le_16"
@@ -50,5 +50,5 @@ class ElkBinarySensor(ElkAttachedEntity, BinarySensorEntity):
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
# Zone in NORMAL state is OFF; any other state is ON
self._attr_is_on = bool(
self._element.logical_status != ZoneLogicalStatus.NORMAL
self._element.logical_status is not ZoneLogicalStatus.NORMAL
)
+2 -2
View File
@@ -104,7 +104,7 @@ class ElkThermostat(ElkEntity, ClimateEntity):
ThermostatMode.EMERGENCY_HEAT,
):
return self._element.heat_setpoint
if self._element.mode == ThermostatMode.COOL:
if self._element.mode is ThermostatMode.COOL:
return self._element.cool_setpoint
return None
@@ -162,6 +162,6 @@ class ElkThermostat(ElkEntity, ClimateEntity):
self._attr_hvac_mode = ELK_TO_HASS_HVAC_MODES[self._element.mode]
if (
self._attr_hvac_mode == HVACMode.OFF
and self._element.fan == ThermostatFan.ON
and self._element.fan is ThermostatFan.ON
):
self._attr_hvac_mode = HVACMode.FAN_ONLY
+1 -1
View File
@@ -56,7 +56,7 @@ class ElkNumberSetting(ElkAttachedEntity, NumberEntity):
def __init__(self, element: Setting, elk: Any, elk_data: ELKM1Data) -> None:
"""Initialize the number setting."""
super().__init__(element, elk, elk_data)
if element.value_format == SettingFormat.TIMER:
if element.value_format is SettingFormat.TIMER:
self._attr_device_class = NumberDeviceClass.DURATION
self._attr_native_unit_of_measurement = UnitOfTime.SECONDS
+6 -6
View File
@@ -75,7 +75,7 @@ async def async_setup_entry(
for setting in elk.settings:
setting = cast(Setting, setting)
domain = (
"time" if setting.value_format == SettingFormat.TIME_OF_DAY else "number"
"time" if setting.value_format is SettingFormat.TIME_OF_DAY else "number"
)
orig_unique_id = generate_unique_id(elk_data.prefix, setting)
@@ -288,7 +288,7 @@ class ElkZone(ElkSensor):
@property
def temperature_unit(self) -> str | None:
"""Return the temperature unit."""
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
return self._temperature_unit
return None
@@ -305,18 +305,18 @@ class ElkZone(ElkSensor):
@property
def native_unit_of_measurement(self) -> str | None:
"""Return the unit of measurement."""
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
return self._temperature_unit
if self._element.definition == ZoneType.ANALOG_ZONE:
if self._element.definition is ZoneType.ANALOG_ZONE:
return UnitOfElectricPotential.VOLT
return None
def _element_changed(self, element: Element, changeset: dict[str, Any]) -> None:
if self._element.definition == ZoneType.TEMPERATURE:
if self._element.definition is ZoneType.TEMPERATURE:
self._attr_native_value = temperature_to_state(
self._element.temperature, UNDEFINED_TEMPERATURE
)
elif self._element.definition == ZoneType.ANALOG_ZONE:
elif self._element.definition is ZoneType.ANALOG_ZONE:
self._attr_native_value = f"{self._element.voltage}"
else:
self._attr_native_value = pretty_const(self._element.logical_status.name)
+1 -1
View File
@@ -66,7 +66,7 @@ class ElkThermostatEMHeat(ElkEntity, SwitchEntity):
@property
def is_on(self) -> bool:
"""Get the current emergency heat status."""
return self._element.mode == ThermostatMode.EMERGENCY_HEAT
return self._element.mode is ThermostatMode.EMERGENCY_HEAT
def _elk_set(self, mode: ThermostatMode) -> None:
"""Set the thermostat mode."""
+1 -1
View File
@@ -30,7 +30,7 @@ async def async_setup_entry(
time_settings = [
setting
for setting in cast(list[Setting], elk.settings)
if setting.value_format == SettingFormat.TIME_OF_DAY
if setting.value_format is SettingFormat.TIME_OF_DAY
]
create_elk_entities(
@@ -96,7 +96,7 @@ def __get_coordinator(call: ServiceCall) -> EnergyZeroDataUpdateCoordinator:
"config_entry": entry_id,
},
)
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unloaded_config_entry",
@@ -125,7 +125,7 @@ async def __get_prices(
data: Electricity | Gas
if price_type == PriceType.GAS:
if price_type is PriceType.GAS:
data = await coordinator.energyzero.get_gas_prices_legacy(
start_date=start,
end_date=end,
@@ -24,7 +24,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: FastdotcomConfigEntry) -
async def _async_finish_startup(hass: HomeAssistant) -> None:
"""Run this only when HA has finished its startup."""
if entry.state == ConfigEntryState.LOADED:
if entry.state is ConfigEntryState.LOADED:
await coordinator.async_refresh()
else:
await coordinator.async_config_entry_first_refresh()
@@ -284,7 +284,7 @@ class FishAudioSubentryFlowHandler(ConfigSubentryFlow):
) -> SubentryFlowResult:
"""Manage initial options."""
entry = self._get_entry()
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
return self.async_abort(reason="entry_not_loaded")
self.client = entry.runtime_data
+1 -1
View File
@@ -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 not entry.state == ConfigEntryState.LOADED:
if entry.state is not 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 == ConfigEntryState.SETUP_RETRY:
) or entry.state is ConfigEntryState.SETUP_RETRY:
self.hass.config_entries.async_schedule_reload(entry.entry_id)
else:
async_dispatcher_send(
+1 -1
View File
@@ -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 == DeviceType.Switch:
if device.device_type is DeviceType.Switch:
entities.append(FluxPowerStateSelect(coordinator.device, entry))
if device.operating_modes:
entities.append(
+1 -1
View File
@@ -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 == DeviceType.Switch:
if coordinator.device.device_type is DeviceType.Switch:
entities.append(FluxSwitch(coordinator, base_unique_id, None))
if entry.data.get(CONF_REMOTE_ACCESS_HOST):
+9 -1
View File
@@ -9,7 +9,12 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, UnitOfDataRate, UnitOfTemperature
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
UnitOfDataRate,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@@ -45,6 +50,7 @@ CALL_SENSORS: tuple[SensorEntityDescription, ...] = (
translation_key="missed",
native_unit_of_measurement="calls",
state_class=SensorStateClass.MEASUREMENT,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -53,6 +59,7 @@ DISK_PARTITION_SENSORS: tuple[SensorEntityDescription, ...] = (
key="partition_free_space",
translation_key="partition_free_space",
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
),
)
@@ -80,6 +87,7 @@ 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,7 +6,6 @@ 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
@@ -19,7 +18,6 @@ SWITCH_DESCRIPTIONS = [
SwitchEntityDescription(
key="wifi",
translation_key="wifi",
entity_category=EntityCategory.CONFIG,
)
]
+3 -3
View File
@@ -212,7 +212,7 @@ class FroniusSolarNet:
inverter_info=_inverter_info,
config_entry=self.config_entry,
)
if self.config_entry.state == ConfigEntryState.LOADED:
if self.config_entry.state is 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 == ConfigEntryState.LOADED:
if self.config_entry.state is 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 == ConfigEntryState.LOADED:
if self.config_entry.state is 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 != ConfigEntryState.LOADED:
if config_entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError(f"{config_entry.title} is not loaded")
coordinators.append(config_entry.runtime_data)
return coordinators
@@ -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 == ProductType.UNKNOWN:
if product_type is ProductType.UNKNOWN:
raise ConfigEntryNotReady("Unable to find product type")
client = Client(get_connection(hass, address), product_type)
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioghost"],
"quality_scale": "gold",
"requirements": ["aioghost==0.4.0"]
"requirements": ["aioghost==0.4.16"]
}
+3 -3
View File
@@ -143,7 +143,7 @@ def _get_entity_descriptions(
local_sync = True
if (
search := data.get(CONF_SEARCH)
) or calendar_item.access_role == AccessRole.FREE_BUSY_READER:
) or calendar_item.access_role is 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 == ResponseStatus.DECLINED
attendee.is_self and attendee.response_status is 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 != event.event_type
and self.entity_description.event_type is not 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 != ConfigEntryState.LOADED:
if self._get_entry().state is not 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 != ConfigEntryState.LOADED:
if self._get_entry().state is not 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 == FileState.FAILED:
if uploaded_file.state is 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 != FileState.ACTIVE
if part.state is not 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 != ConfigEntryState.LOADED:
if self._get_entry().state is not 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 != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
continue
for coord in entry.runtime_data.devices.values():
+2 -2
View File
@@ -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 == AwesomeVersionStrategy.UNKNOWN:
if version.dev or version.strategy is 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 == AwesomeVersionStrategy.UNKNOWN:
if version.dev or version.strategy is AwesomeVersionStrategy.UNKNOWN:
return "https://github.com/home-assistant/supervisor/commits/main"
return f"https://github.com/home-assistant/supervisor/releases/tag/{version}"
+1 -1
View File
@@ -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 not entry.state == ConfigEntryState.LOADED:
if not entry or entry.state is not ConfigEntryState.LOADED:
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="integration_not_loaded"
)
@@ -418,10 +418,10 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
otbr_manager = get_otbr_addon_manager(self.hass)
addon_info = await self._async_get_addon_info(otbr_manager)
if addon_info.state == AddonState.NOT_INSTALLED:
if addon_info.state is AddonState.NOT_INSTALLED:
return await self.async_step_install_otbr_addon()
if addon_info.state == AddonState.RUNNING:
if addon_info.state is AddonState.RUNNING:
await otbr_manager.async_stop_addon()
return await self.async_step_start_otbr_addon()
@@ -111,7 +111,7 @@ class WaitingAddonManager(AddonManager):
info = None
# Do not try to uninstall an addon if it is already uninstalled
if info is not None and info.state == AddonState.NOT_INSTALLED:
if info is not None and info.state is AddonState.NOT_INSTALLED:
return
await self.async_uninstall_addon()
@@ -383,7 +383,7 @@ class OptionsFlowHandler(OptionsFlow, ABC):
multipan_manager = await get_multiprotocol_addon_manager(self.hass)
addon_info = await self._async_get_addon_info(multipan_manager)
if addon_info.state == AddonState.NOT_INSTALLED:
if addon_info.state is AddonState.NOT_INSTALLED:
return await self.async_step_addon_not_installed()
return await self.async_step_addon_installed()
@@ -691,10 +691,10 @@ class OptionsFlowHandler(OptionsFlow, ABC):
flasher_manager = get_flasher_addon_manager(self.hass)
addon_info = await self._async_get_addon_info(flasher_manager)
if addon_info.state == AddonState.NOT_INSTALLED:
if addon_info.state is AddonState.NOT_INSTALLED:
return await self.async_step_install_flasher_addon()
if addon_info.state == AddonState.NOT_RUNNING:
if addon_info.state is AddonState.NOT_RUNNING:
return await self.async_step_configure_flasher_addon()
# If the addon is already installed and running, fail
@@ -907,7 +907,7 @@ async def check_multi_pan_addon(hass: HomeAssistant) -> None:
# Request the addon to start if it's not started
# `async_start_addon` returns as soon as the start request has been sent
# and does not wait for the addon to be started, so we raise below
if addon_info.state == AddonState.NOT_RUNNING:
if addon_info.state is AddonState.NOT_RUNNING:
await multipan_manager.async_start_addon()
if addon_info.state not in (AddonState.NOT_INSTALLED, AddonState.RUNNING):
@@ -927,7 +927,7 @@ async def multi_pan_addon_using_device(hass: HomeAssistant, device_path: str) ->
multipan_manager = await get_multiprotocol_addon_manager(hass)
addon_info: AddonInfo = await multipan_manager.async_get_addon_info()
if addon_info.state != AddonState.RUNNING:
if addon_info.state is not AddonState.RUNNING:
return False
if addon_info.options["device"] != device_path:
@@ -124,7 +124,7 @@ class OwningAddon:
except AddonError:
return False
else:
return addon_info.state == AddonState.RUNNING
return addon_info.state is AddonState.RUNNING
@asynccontextmanager
async def temporarily_stop(self, hass: HomeAssistant) -> AsyncGenerator[None]:
@@ -137,7 +137,7 @@ class OwningAddon:
yield
return
if addon_info.state != AddonState.RUNNING:
if addon_info.state is not AddonState.RUNNING:
yield
return
@@ -173,7 +173,7 @@ class OwningIntegration:
yield
return
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
yield
return
@@ -213,7 +213,7 @@ async def get_otbr_addon_firmware_info(
except AddonError:
return None
if otbr_addon_info.state == AddonState.NOT_INSTALLED:
if otbr_addon_info.state is AddonState.NOT_INSTALLED:
return None
if (otbr_path := otbr_addon_info.options.get("device")) is None:
@@ -238,7 +238,7 @@ async def get_z2m_addon_firmware_info(
except AddonError:
return None
if z2m_addon_info.state == AddonState.NOT_INSTALLED:
if z2m_addon_info.state is AddonState.NOT_INSTALLED:
return None
serial = z2m_addon_info.options.get("serial")
@@ -286,7 +286,7 @@ async def guess_hardware_owners(
except AddonError:
pass
else:
if multipan_addon_info.state != AddonState.NOT_INSTALLED:
if multipan_addon_info.state is not AddonState.NOT_INSTALLED:
multipan_path = multipan_addon_info.options.get("device")
if multipan_path is not None:
@@ -122,7 +122,7 @@ class HomeeConfigFlow(ConfigFlow, domain=DOMAIN):
existing_entry = await self.async_set_unique_id(self._name)
if (
existing_entry
and existing_entry.state == ConfigEntryState.LOADED
and existing_entry.state is ConfigEntryState.LOADED
and existing_entry.runtime_data.connected
and existing_entry.data[CONF_HOST] != self._host
):
@@ -298,7 +298,7 @@ class HKDevice:
# yet.
attempts = None if self.hass.state is CoreState.running else 1
if (
transport == Transport.BLE
transport is Transport.BLE
and pairing.accessories
and pairing.accessories.has_aid(1)
):
@@ -328,7 +328,7 @@ class HKDevice:
)
entry.async_on_unload(self._async_cancel_subscription_timer)
if transport != Transport.BLE:
if transport is not Transport.BLE:
# Although async_populate_accessories_state fetched the accessory database,
# the /accessories endpoint may return cached values from the accessory's
# perspective. For example, Ecobee thermostats may report stale temperature
@@ -349,7 +349,7 @@ class HKDevice:
await self.async_process_entity_map()
if transport != Transport.BLE:
if transport is not Transport.BLE:
# Start regular polling after entity map is processed
self._async_start_polling()
@@ -359,7 +359,7 @@ class HKDevice:
self.async_set_available_state(self.pairing.is_available)
if transport == Transport.BLE:
if transport is Transport.BLE:
# If we are using BLE, we need to periodically check of the
# BLE device is available since we won't get callbacks
# when it goes away since we HomeKit supports disconnected
@@ -420,7 +420,7 @@ class HKDevice:
identifiers.add((IDENTIFIER_SERIAL_NUMBER, accessory.serial_number))
connections: set[tuple[str, str]] = set()
if self.pairing.transport == Transport.BLE and (
if self.pairing.transport is Transport.BLE and (
discovery := self.pairing.controller.discoveries.get(
normalize_hkid(self.unique_id)
)
@@ -622,7 +622,7 @@ class HKDevice:
current_unique_id.add((accessory.aid, service.iid, None))
for char in service.characteristics:
if self.pairing.transport != Transport.BLE:
if self.pairing.transport is not Transport.BLE:
if char.type == CharacteristicsTypes.THREAD_CONTROL_POINT:
continue
@@ -1057,7 +1057,7 @@ class HKDevice:
@property
def is_unprovisioned_thread_device(self) -> bool:
"""Is this a thread capable device not connected by CoAP."""
if self.pairing.controller.transport_type != TransportType.BLE:
if self.pairing.controller.transport_type is not TransportType.BLE:
return False
if not self.entity_map.aid(1).services.first(
@@ -1069,7 +1069,7 @@ class HKDevice:
async def async_thread_provision(self) -> None:
"""Migrate a HomeKit pairing to CoAP (Thread)."""
if self.pairing.controller.transport_type == TransportType.COAP:
if self.pairing.controller.transport_type is TransportType.COAP:
raise HomeAssistantError("Already connected to a thread network")
if not (dataset := await async_get_preferred_dataset(self.hass)):
@@ -700,7 +700,7 @@ async def async_setup_entry(
@callback
def async_add_accessory(accessory: Accessory) -> bool:
if conn.pairing.transport != Transport.BLE:
if conn.pairing.transport is not Transport.BLE:
return False
accessory_info = accessory.services.first(
@@ -69,7 +69,7 @@ class HusqvarnaAutomowerBleConfigFlow(ConfigFlow, domain=DOMAIN):
await async_get_manufacturer_data({discovery_info.address})
)[discovery_info.address]
if manufacturer_data.product_type != ProductType.MOWER:
if manufacturer_data.product_type is not ProductType.MOWER:
LOGGER.debug(
"Unsupported device: %s (%s)", manufacturer_data, discovery_info
)
@@ -12,6 +12,6 @@
"documentation": "https://www.home-assistant.io/integrations/indevolt",
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "silver",
"quality_scale": "platinum",
"requirements": ["indevolt-api==1.8.1"]
}
@@ -567,7 +567,7 @@ class IntegrationSensor(RestoreSensor):
assert old_timestamp is not None
elapsed_seconds = Decimal(
(new_timestamp - old_timestamp).total_seconds()
if self._last_integration_trigger == _IntegrationTrigger.StateEvent
if self._last_integration_trigger is _IntegrationTrigger.StateEvent
else (new_timestamp - self._last_integration_time).total_seconds()
)
@@ -192,11 +192,11 @@ async def async_update_options(
current_control_mode = fireplace.control_mode
# Only update modes that actually changed
if new_read_mode != current_read_mode:
if new_read_mode is not current_read_mode:
LOGGER.debug("Updating read mode: %s -> %s", current_read_mode, new_read_mode)
await fireplace.set_read_mode(new_read_mode)
if new_control_mode != current_control_mode:
if new_control_mode is not current_control_mode:
LOGGER.debug(
"Updating control mode: %s -> %s", current_control_mode, new_control_mode
)
+3 -3
View File
@@ -126,19 +126,19 @@ class IottyShutter(IottyEntity, CoverEntity):
self._iotty_device.percentage,
)
return (
self._iotty_device.status == ShutterState.STATIONARY
self._iotty_device.status is ShutterState.STATIONARY
and self._iotty_device.percentage == 0
)
@property
def is_opening(self) -> bool:
"""Return true if the Shutter is opening."""
return self._iotty_device.status == ShutterState.OPENING
return self._iotty_device.status is ShutterState.OPENING
@property
def is_closing(self) -> bool:
"""Return true if the Shutter is closing."""
return self._iotty_device.status == ShutterState.CLOSING
return self._iotty_device.status is ShutterState.CLOSING
@property
def supported_features(self) -> CoverEntityFeature:
+3 -3
View File
@@ -380,7 +380,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
self._attr_fan_modes = [fan_zero_mode, FAN_LOW, FAN_HIGH]
elif fan_max_step == 1:
self._attr_fan_modes = [fan_zero_mode, FAN_ON]
elif device.fan_speed_mode == FanSpeedMode.STEP:
elif device.fan_speed_mode is FanSpeedMode.STEP:
self._attr_fan_modes = [fan_zero_mode] + [
str(i) for i in range(1, fan_max_step + 1)
]
@@ -550,7 +550,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
if not fan_speed or self._attr_fan_modes is None:
return self.fan_zero_mode
if self._device.fan_speed_mode == FanSpeedMode.STEP:
if self._device.fan_speed_mode is FanSpeedMode.STEP:
return self._attr_fan_modes[fan_speed]
# Find the closest fan mode percentage
@@ -570,7 +570,7 @@ class _KnxClimate(ClimateEntity, _KnxEntityBase):
fan_mode_index = self._attr_fan_modes.index(fan_mode)
if self._device.fan_speed_mode == FanSpeedMode.STEP:
if self._device.fan_speed_mode is FanSpeedMode.STEP:
await self._device.set_fan_speed(fan_mode_index)
return
+2 -6
View File
@@ -373,7 +373,6 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
)
)
}
return self.async_show_form(step_id="tunnel", data_schema=vol.Schema(fields))
async def async_step_tcp_tunnel_endpoint(
@@ -459,7 +458,7 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
errors[CONF_HOST] = "invalid_ip_address"
_local_ip = None
if _local := user_input.get(CONF_KNX_LOCAL_IP):
if _local := (user_input.get(CONF_KNX_LOCAL_IP) or None):
try:
_local_ip = await xknx_validate_ip(_local)
ip_v4_validator(_local_ip, multicast=False)
@@ -606,7 +605,6 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
selector.TextSelectorConfig(type=selector.TextSelectorType.PASSWORD),
),
}
return self.async_show_form(
step_id="secure_tunnel_manual",
data_schema=vol.Schema(fields),
@@ -662,7 +660,6 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Coerce(int),
),
}
return self.async_show_form(
step_id="secure_routing_manual",
data_schema=vol.Schema(fields),
@@ -843,7 +840,7 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
ip_v4_validator(_multicast_group, multicast=True)
except vol.Invalid:
errors[CONF_KNX_MCAST_GRP] = "invalid_ip_address"
if _local := user_input.get(CONF_KNX_LOCAL_IP):
if _local := (user_input.get(CONF_KNX_LOCAL_IP) or None):
try:
_local_ip = await xknx_validate_ip(_local)
ip_v4_validator(_local_ip, multicast=False)
@@ -891,7 +888,6 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
vol.Required(CONF_KNX_MCAST_PORT, default=_multicast_port): _PORT_SELECTOR,
vol.Optional(CONF_KNX_LOCAL_IP): _IP_SELECTOR,
}
return self.async_show_form(
step_id="routing", data_schema=vol.Schema(fields), errors=errors
)
+1 -1
View File
@@ -258,7 +258,7 @@ class KNXModule:
def connection_state_changed_cb(self, state: XknxConnectionState) -> None:
"""Call invoked after a KNX connection state change was received."""
self.connected = state == XknxConnectionState.CONNECTED
self.connected = state is XknxConnectionState.CONNECTED
for device in self.xknx.devices:
device.after_update()
+3 -3
View File
@@ -246,7 +246,7 @@ def async_host_event_received(
):
_LOGGER.info('The connection to host "%s" has been lost', config_entry.title)
hass.async_create_task(reload_config_entry())
elif event == LcnEvent.BUS_DISCONNECTED:
elif event is LcnEvent.BUS_DISCONNECTED:
_LOGGER.info(
'The connection to the LCN bus via host "%s" has been disconnected',
config_entry.title,
@@ -296,7 +296,7 @@ def _async_fire_access_control_event(
if device is not None:
event_data.update({CONF_DEVICE_ID: device.id})
if inp.periphery == pypck.lcn_defs.AccessControlPeriphery.TRANSMITTER:
if inp.periphery is pypck.lcn_defs.AccessControlPeriphery.TRANSMITTER:
event_data.update(
{
"level": inp.level,
@@ -317,7 +317,7 @@ def _async_fire_send_keys_event(
) -> None:
"""Fire send_keys event."""
for table, action in enumerate(inp.actions):
if action == pypck.lcn_defs.SendKeyCommand.DONTSEND:
if action is pypck.lcn_defs.SendKeyCommand.DONTSEND:
continue
for key, selected in enumerate(inp.keys):
+3 -3
View File
@@ -116,7 +116,7 @@ class LcnClimate(LcnEntity, ClimateEntity):
# Config schema only allows for:
# UnitOfTemperature.CELSIUS and
# UnitOfTemperature.FAHRENHEIT
if self.unit == pypck.lcn_defs.VarUnit.FAHRENHEIT:
if self.unit is pypck.lcn_defs.VarUnit.FAHRENHEIT:
return UnitOfTemperature.FAHRENHEIT
return UnitOfTemperature.CELSIUS
@@ -188,11 +188,11 @@ class LcnClimate(LcnEntity, ClimateEntity):
if not isinstance(input_obj, pypck.inputs.ModStatusVar):
return
self._attr_available = True
if input_obj.get_var() == self.variable:
if input_obj.get_var() is self.variable:
self._attr_current_temperature = float(
input_obj.get_value().to_var_unit(self.unit)
)
elif input_obj.get_var() == self.setpoint:
elif input_obj.get_var() is self.setpoint:
self._is_on = not input_obj.get_value().is_locked_regulator()
if self._is_on:
self._attr_target_temperature = float(
+3 -3
View File
@@ -191,7 +191,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
)
)
if self.positioning_mode != pypck.lcn_defs.MotorPositioningMode.NONE:
if self.positioning_mode is not pypck.lcn_defs.MotorPositioningMode.NONE:
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
self.motor = pypck.lcn_defs.MotorPort[config[CONF_DOMAIN_DATA][CONF_MOTOR]]
@@ -267,7 +267,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
| None,
]
] = [self.device_connection.request_status_relays(SCAN_INTERVAL.seconds)]
if self.positioning_mode == pypck.lcn_defs.MotorPositioningMode.BS4:
if self.positioning_mode is pypck.lcn_defs.MotorPositioningMode.BS4:
coros.append(
self.device_connection.request_status_motor_position(
self.motor, self.positioning_mode, SCAN_INTERVAL.seconds
@@ -282,7 +282,7 @@ class LcnRelayCover(LcnEntity, CoverEntity):
self._attr_is_opening = input_obj.is_opening(self.motor.value)
self._attr_is_closing = input_obj.is_closing(self.motor.value)
if self.positioning_mode == pypck.lcn_defs.MotorPositioningMode.NONE:
if self.positioning_mode is pypck.lcn_defs.MotorPositioningMode.NONE:
self._attr_is_closed = input_obj.is_assumed_closed(self.motor.value)
self.async_write_ha_state()
elif (
+1 -1
View File
@@ -144,7 +144,7 @@ class LcnVariableSensor(LcnEntity, SensorEntity):
"""Set sensor value when LCN input object (command) is received."""
if (
not isinstance(input_obj, pypck.inputs.ModStatusVar)
or input_obj.get_var() != self.variable
or input_obj.get_var() is not self.variable
):
return
self._attr_available = True
+1 -1
View File
@@ -330,7 +330,7 @@ class SendKeys(LcnServiceCall):
if (delay_time := service.data[CONF_TIME]) != 0:
hit = pypck.lcn_defs.SendKeyCommand.HIT
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] != hit:
if pypck.lcn_defs.SendKeyCommand[service.data[CONF_STATE]] is not hit:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_send_keys_action",
+1 -1
View File
@@ -200,7 +200,7 @@ class LcnRegulatorLockSwitch(LcnEntity, SwitchEntity):
"""Set switch state when LCN input object (command) is received."""
if (
not isinstance(input_obj, pypck.inputs.ModStatusVar)
or input_obj.get_var() != self.setpoint_variable
or input_obj.get_var() is not self.setpoint_variable
):
return
self._attr_available = True
@@ -77,7 +77,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LunatoneConfigEntry) ->
await coordinator_info.async_config_entry_first_refresh()
if info_api.data is None or info_api.serial_number is None:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryError(
translation_domain=DOMAIN, translation_key="missing_device_info"
)
@@ -35,5 +35,10 @@
"description": "Enter the URL of your Lunatone device.\nHome Assistant will use this address to connect to the device API."
}
}
},
"exceptions": {
"missing_device_info": {
"message": "Unable to read device information. Please verify the device's network connection."
}
}
}
@@ -54,4 +54,4 @@ class LutronOccupancySensor(LutronDevice, BinarySensorEntity):
def _update_attrs(self) -> None:
"""Update the state attributes."""
self._attr_is_on = self._lutron_device.state == OccupancyGroup.State.OCCUPIED
self._attr_is_on = self._lutron_device.state is OccupancyGroup.State.OCCUPIED
@@ -79,7 +79,7 @@ class LutronCasetaShade(LutronCasetaUpdatableEntity, CoverEntity):
"""Stop the cover."""
# Send appropriate directional command before stop to ensure it works correctly
# Use tracked direction if moving, otherwise use position-based heuristic
if self._movement_direction == ShadeMovementDirection.OPENING or (
if self._movement_direction is ShadeMovementDirection.OPENING or (
self._movement_direction in (ShadeMovementDirection.STOPPED, None)
and self.current_cover_position >= 50
):
+4 -4
View File
@@ -179,13 +179,13 @@ async def _client_listen(
try:
await matter_client.start_listening(init_ready)
except MatterError as err:
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
raise
LOGGER.error("Failed to listen: %s", err)
except Exception as err:
# We need to guard against unknown exceptions to not crash this task.
LOGGER.exception("Unexpected exception: %s", err)
if entry.state != ConfigEntryState.LOADED:
if entry.state is not ConfigEntryState.LOADED:
raise
if not hass.is_stopping:
@@ -293,14 +293,14 @@ async def _async_ensure_addon_running(
addon_state = addon_info.state
if addon_state == AddonState.NOT_INSTALLED:
if addon_state is AddonState.NOT_INSTALLED:
addon_manager.async_schedule_install_setup_addon(
addon_info.options,
catch_error=True,
)
raise ConfigEntryNotReady
if addon_state == AddonState.NOT_RUNNING:
if addon_state is AddonState.NOT_RUNNING:
addon_manager.async_schedule_start_addon(catch_error=True)
raise ConfigEntryNotReady
@@ -288,10 +288,10 @@ class MatterConfigFlow(ConfigFlow, domain=DOMAIN):
addon_info = await self._async_get_addon_info()
if addon_info.state == AddonState.RUNNING:
if addon_info.state is AddonState.RUNNING:
return await self.async_step_finish_addon_setup()
if addon_info.state == AddonState.NOT_RUNNING:
if addon_info.state is AddonState.NOT_RUNNING:
return await self.async_step_start_addon()
return await self.async_step_install_addon()

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