Merge branch 'dev' into state_selector_service_select

This commit is contained in:
Paul Bottein 2025-07-21 14:19:34 +02:00 committed by GitHub
commit bf6ffb83dc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
382 changed files with 6556 additions and 2345 deletions

View File

@ -45,6 +45,12 @@ rules:
**When Reviewing/Creating Code**: Always check the integration's quality scale level and exemption status before applying rules. **When Reviewing/Creating Code**: Always check the integration's quality scale level and exemption status before applying rules.
## Code Review Guidelines
**When reviewing code, do NOT comment on:**
- **Missing imports** - We use static analysis tooling to catch that
- **Code formatting** - We have ruff as a formatting tool that will catch those if needed (unless specifically instructed otherwise in these instructions)
## Python Requirements ## Python Requirements
- **Compatibility**: Python 3.13+ - **Compatibility**: Python 3.13+

View File

@ -6,3 +6,6 @@ updates:
interval: daily interval: daily
time: "06:00" time: "06:00"
open-pull-requests-limit: 10 open-pull-requests-limit: 10
labels:
- dependency
- github_actions

View File

@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI - name: Detect duplicates using AI
id: ai_detection id: ai_detection
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true' if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
uses: actions/ai-inference@v1.1.0 uses: actions/ai-inference@v1.2.3
with: with:
model: openai/gpt-4o model: openai/gpt-4o
system-prompt: | system-prompt: |

View File

@ -57,7 +57,7 @@ jobs:
- name: Detect language using AI - name: Detect language using AI
id: ai_language_detection id: ai_language_detection
if: steps.detect_language.outputs.should_continue == 'true' if: steps.detect_language.outputs.should_continue == 'true'
uses: actions/ai-inference@v1.1.0 uses: actions/ai-inference@v1.2.3
with: with:
model: openai/gpt-4o-mini model: openai/gpt-4o-mini
system-prompt: | system-prompt: |

4
CODEOWNERS generated
View File

@ -684,8 +684,8 @@ build.json @home-assistant/supervisor
/tests/components/husqvarna_automower/ @Thomas55555 /tests/components/husqvarna_automower/ @Thomas55555
/homeassistant/components/husqvarna_automower_ble/ @alistair23 /homeassistant/components/husqvarna_automower_ble/ @alistair23
/tests/components/husqvarna_automower_ble/ @alistair23 /tests/components/husqvarna_automower_ble/ @alistair23
/homeassistant/components/huum/ @frwickst /homeassistant/components/huum/ @frwickst @vincentwolsink
/tests/components/huum/ @frwickst /tests/components/huum/ @frwickst @vincentwolsink
/homeassistant/components/hvv_departures/ @vigonotion /homeassistant/components/hvv_departures/ @vigonotion
/tests/components/hvv_departures/ @vigonotion /tests/components/hvv_departures/ @vigonotion
/homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan /homeassistant/components/hydrawise/ @dknowles2 @thomaskistler @ptcryan

View File

@ -6,6 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/airgradient", "documentation": "https://www.home-assistant.io/integrations/airgradient",
"integration_type": "device", "integration_type": "device",
"iot_class": "local_polling", "iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["airgradient==0.9.2"], "requirements": ["airgradient==0.9.2"],
"zeroconf": ["_airgradient._tcp.local."] "zeroconf": ["_airgradient._tcp.local."]
} }

View File

@ -14,9 +14,9 @@ rules:
status: exempt status: exempt
comment: | comment: |
This integration does not provide additional actions. This integration does not provide additional actions.
docs-high-level-description: todo docs-high-level-description: done
docs-installation-instructions: todo docs-installation-instructions: done
docs-removal-instructions: todo docs-removal-instructions: done
entity-event-setup: entity-event-setup:
status: exempt status: exempt
comment: | comment: |
@ -34,7 +34,7 @@ rules:
docs-configuration-parameters: docs-configuration-parameters:
status: exempt status: exempt
comment: No options to configure comment: No options to configure
docs-installation-parameters: todo docs-installation-parameters: done
entity-unavailable: done entity-unavailable: done
integration-owner: done integration-owner: done
log-when-unavailable: done log-when-unavailable: done
@ -43,23 +43,19 @@ rules:
status: exempt status: exempt
comment: | comment: |
This integration does not require authentication. This integration does not require authentication.
test-coverage: todo test-coverage: done
# Gold # Gold
devices: done devices: done
diagnostics: done diagnostics: done
discovery-update-info: discovery-update-info: done
status: todo discovery: done
comment: DHCP is still possible docs-data-update: done
discovery: docs-examples: done
status: todo docs-known-limitations: done
comment: DHCP is still possible docs-supported-devices: done
docs-data-update: todo docs-supported-functions: done
docs-examples: todo docs-troubleshooting: done
docs-known-limitations: todo docs-use-cases: done
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices: dynamic-devices:
status: exempt status: exempt
comment: | comment: |

View File

@ -45,9 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bo
# Store Entity and Initialize Platforms # Store Entity and Initialize Platforms
entry.runtime_data = coordinator entry.runtime_data = coordinator
# Listen for option changes
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Clean up unused device entries with no entities # Clean up unused device entries with no entities
@ -88,8 +85,3 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -13,7 +13,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -126,7 +126,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
return AirNowOptionsFlowHandler() return AirNowOptionsFlowHandler()
class AirNowOptionsFlowHandler(OptionsFlow): class AirNowOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow for AirNow.""" """Handle an options flow for AirNow."""
async def async_step_init( async def async_step_init(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioairzone_cloud"], "loggers": ["aioairzone_cloud"],
"requirements": ["aioairzone-cloud==0.6.14"] "requirements": ["aioairzone-cloud==0.6.15"]
} }

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["aioamazondevices"], "loggers": ["aioamazondevices"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aioamazondevices==3.2.10"] "requirements": ["aioamazondevices==3.5.0"]
} }

View File

@ -55,7 +55,6 @@ async def async_setup_entry(
entry.runtime_data = AnalyticsInsightsData(coordinator=coordinator, names=names) entry.runtime_data = AnalyticsInsightsData(coordinator=coordinator, names=names)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
@ -65,10 +64,3 @@ async def async_unload_entry(
) -> bool: ) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(
hass: HomeAssistant, entry: AnalyticsInsightsConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -11,7 +11,11 @@ from python_homeassistant_analytics import (
from python_homeassistant_analytics.models import Environment, IntegrationType from python_homeassistant_analytics.models import Environment, IntegrationType
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
@ -129,7 +133,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow): class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
"""Handle Homeassistant Analytics options.""" """Handle Homeassistant Analytics options."""
async def async_step_init( async def async_step_init(

View File

@ -68,7 +68,6 @@ async def async_setup_entry(
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
) )
entry.async_on_unload(entry.add_update_listener(async_update_options))
entry.async_on_unload(api.disconnect) entry.async_on_unload(api.disconnect)
return True return True
@ -80,13 +79,3 @@ async def async_unload_entry(
"""Unload a config entry.""" """Unload a config entry."""
_LOGGER.debug("async_unload_entry: %s", entry.data) _LOGGER.debug("async_unload_entry: %s", entry.data)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> None:
"""Handle options update."""
_LOGGER.debug(
"async_update_options: data: %s options: %s", entry.data, entry.options
)
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -19,7 +19,7 @@ from homeassistant.config_entries import (
SOURCE_RECONFIGURE, SOURCE_RECONFIGURE,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
@ -116,10 +116,10 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
pin = user_input["pin"] pin = user_input["pin"]
await self.api.async_finish_pairing(pin) await self.api.async_finish_pairing(pin)
if self.source == SOURCE_REAUTH: if self.source == SOURCE_REAUTH:
await self.hass.config_entries.async_reload( return self.async_update_reload_and_abort(
self._get_reauth_entry().entry_id self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
) )
return self.async_abort(reason="reauth_successful")
return self.async_create_entry( return self.async_create_entry(
title=self.name, title=self.name,
data={ data={
@ -243,7 +243,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
return AndroidTVRemoteOptionsFlowHandler(config_entry) return AndroidTVRemoteOptionsFlowHandler(config_entry)
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow): class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
"""Android TV Remote options flow.""" """Android TV Remote options flow."""
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None: def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj", "documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["arcam"], "loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.1"], "requirements": ["arcam-fmj==1.8.2"],
"ssdp": [ "ssdp": [
{ {
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@ -0,0 +1 @@
"""Bauknecht virtual integration."""

View File

@ -0,0 +1,6 @@
{
"domain": "bauknecht",
"name": "Bauknecht",
"integration_type": "virtual",
"supported_by": "whirlpool"
}

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/blue_current", "documentation": "https://www.home-assistant.io/integrations/blue_current",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["bluecurrent_api"], "loggers": ["bluecurrent_api"],
"requirements": ["bluecurrent-api==1.2.3"] "requirements": ["bluecurrent-api==1.2.4"]
} }

View File

@ -40,10 +40,11 @@ from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
VALID_REPAIR_TRANSLATION_KEYS = { VALID_REPAIR_TRANSLATION_KEYS = {
"connection_error",
"no_subscription", "no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration", "reset_bad_custom_domain_configuration",
"subscription_expired", "subscription_expired",
"warn_bad_custom_domain_configuration",
} }

View File

@ -71,7 +71,7 @@ _CLOUD_ERRORS: dict[
] = { ] = {
TimeoutError: ( TimeoutError: (
HTTPStatus.BAD_GATEWAY, HTTPStatus.BAD_GATEWAY,
"Unable to reach the Home Assistant cloud.", "Unable to reach the Home Assistant Cloud.",
), ),
aiohttp.ClientError: ( aiohttp.ClientError: (
HTTPStatus.INTERNAL_SERVER_ERROR, HTTPStatus.INTERNAL_SERVER_ERROR,

View File

@ -13,6 +13,6 @@
"integration_type": "system", "integration_type": "system",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"], "loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.106.0"], "requirements": ["hass-nabucasa==0.107.1"],
"single_config_entry": true "single_config_entry": true
} }

View File

@ -62,6 +62,10 @@
} }
} }
}, },
"connection_error": {
"title": "No connection",
"description": "You do not have a connection to Home Assistant Cloud. Check your network."
},
"no_subscription": { "no_subscription": {
"title": "No subscription detected", "title": "No subscription detected",
"description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}." "description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}."

View File

@ -10,8 +10,6 @@ from typing import Any
from jsonpath import jsonpath from jsonpath import jsonpath
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import ( from homeassistant.const import (
CONF_COMMAND, CONF_COMMAND,
CONF_NAME, CONF_NAME,
@ -188,16 +186,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.entity_id, variables, None self.entity_id, variables, None
) )
if self.device_class not in { self._set_native_value_with_possible_timestamp(value)
SensorDeviceClass.DATE,
SensorDeviceClass.TIMESTAMP,
}:
self._attr_native_value = value
elif value is not None:
self._attr_native_value = async_parse_date_datetime(
value, self.entity_id, self.device_class
)
self._process_manual_data(variables) self._process_manual_data(variables)
self.async_write_ha_state() self.async_write_ha_state()

View File

@ -54,16 +54,20 @@ class Control4RuntimeData:
type Control4ConfigEntry = ConfigEntry[Control4RuntimeData] type Control4ConfigEntry = ConfigEntry[Control4RuntimeData]
async def call_c4_api_retry(func, *func_args): # noqa: RET503 async def call_c4_api_retry(func, *func_args):
"""Call C4 API function and retry on failure.""" """Call C4 API function and retry on failure."""
# Ruff doesn't understand this loop - the exception is always raised after the retries exc = None
for i in range(API_RETRY_TIMES): for i in range(API_RETRY_TIMES):
try: try:
return await func(*func_args) return await func(*func_args)
except client_exceptions.ClientError as exception: except client_exceptions.ClientError as exception:
_LOGGER.error("Error connecting to Control4 account API: %s", exception) _LOGGER.error(
if i == API_RETRY_TIMES - 1: "Try: %d, Error connecting to Control4 account API: %s",
raise ConfigEntryNotReady(exception) from exception i + 1,
exception,
)
exc = exception
raise ConfigEntryNotReady(exc) from exc
async def async_setup_entry(hass: HomeAssistant, entry: Control4ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: Control4ConfigEntry) -> bool:
@ -141,21 +145,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: Control4ConfigEntry) ->
ui_configuration=ui_configuration, ui_configuration=ui_configuration,
) )
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def update_listener(
hass: HomeAssistant, config_entry: Control4ConfigEntry
) -> None:
"""Update when config_entry options update."""
_LOGGER.debug("Config entry was updated, rerunning setup")
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: Control4ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: Control4ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -11,7 +11,11 @@ from pyControl4.director import C4Director
from pyControl4.error_handling import NotFound, Unauthorized from pyControl4.error_handling import NotFound, Unauthorized
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -153,7 +157,7 @@ class Control4ConfigFlow(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler() return OptionsFlowHandler()
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option flow for Control4.""" """Handle a option flow for Control4."""
async def async_step_init( async def async_step_init(

View File

@ -53,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DenonavrConfigEntry) ->
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
receiver = connect_denonavr.receiver receiver = connect_denonavr.receiver
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.runtime_data = receiver entry.runtime_data = receiver
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -100,10 +98,3 @@ async def async_unload_entry(
_LOGGER.debug("Removing zone3 from DenonAvr") _LOGGER.debug("Removing zone3 from DenonAvr")
return unload_ok return unload_ok
async def update_listener(
hass: HomeAssistant, config_entry: DenonavrConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -10,7 +10,11 @@ import denonavr
from denonavr.exceptions import AvrNetworkError, AvrTimoutError from denonavr.exceptions import AvrNetworkError, AvrTimoutError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TYPE from homeassistant.const import CONF_HOST, CONF_MODEL, CONF_TYPE
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.httpx_client import get_async_client from homeassistant.helpers.httpx_client import get_async_client
@ -51,7 +55,7 @@ DEFAULT_USE_TELNET_NEW_INSTALL = True
CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str}) CONFIG_SCHEMA = vol.Schema({vol.Optional(CONF_HOST): str})
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithReload):
"""Options for the component.""" """Options for the component."""
async def async_step_init( async def async_step_init(

View File

@ -320,7 +320,12 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
# changed state, then we know it will still be zero. # changed state, then we know it will still be zero.
return return
schedule_max_sub_interval_exceeded(new_state) schedule_max_sub_interval_exceeded(new_state)
calc_derivative(new_state, new_state.state, event.data["old_last_reported"]) calc_derivative(
new_state,
new_state.state,
event.data["last_reported"],
event.data["old_last_reported"],
)
@callback @callback
def on_state_changed(event: Event[EventStateChangedData]) -> None: def on_state_changed(event: Event[EventStateChangedData]) -> None:
@ -334,19 +339,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
schedule_max_sub_interval_exceeded(new_state) schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"] old_state = event.data["old_state"]
if old_state is not None: if old_state is not None:
calc_derivative(new_state, old_state.state, old_state.last_reported) calc_derivative(
new_state,
old_state.state,
new_state.last_updated,
old_state.last_reported,
)
else: else:
# On first state change from none, update availability # On first state change from none, update availability
self.async_write_ha_state() self.async_write_ha_state()
def calc_derivative( def calc_derivative(
new_state: State, old_value: str, old_last_reported: datetime new_state: State,
old_value: str,
new_timestamp: datetime,
old_timestamp: datetime,
) -> None: ) -> None:
"""Handle the sensor state changes.""" """Handle the sensor state changes."""
if not _is_decimal_state(old_value): if not _is_decimal_state(old_value):
if self._last_valid_state_time: if self._last_valid_state_time:
old_value = self._last_valid_state_time[0] old_value = self._last_valid_state_time[0]
old_last_reported = self._last_valid_state_time[1] old_timestamp = self._last_valid_state_time[1]
else: else:
# Sensor becomes valid for the first time, just keep the restored value # Sensor becomes valid for the first time, just keep the restored value
self.async_write_ha_state() self.async_write_ha_state()
@ -358,12 +371,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"" if unit is None else unit "" if unit is None else unit
) )
self._prune_state_list(new_state.last_reported) self._prune_state_list(new_timestamp)
try: try:
elapsed_time = ( elapsed_time = (new_timestamp - old_timestamp).total_seconds()
new_state.last_reported - old_last_reported
).total_seconds()
delta_value = Decimal(new_state.state) - Decimal(old_value) delta_value = Decimal(new_state.state) - Decimal(old_value)
new_derivative = ( new_derivative = (
delta_value delta_value
@ -392,12 +403,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
return return
# add latest derivative to the window list # add latest derivative to the window list
self._state_list.append( self._state_list.append((old_timestamp, new_timestamp, new_derivative))
(old_last_reported, new_state.last_reported, new_derivative)
)
self._last_valid_state_time = ( self._last_valid_state_time = (
new_state.state, new_state.state,
new_state.last_reported, new_timestamp,
) )
# If outside of time window just report derivative (is the same as modeling it in the window), # If outside of time window just report derivative (is the same as modeling it in the window),
@ -405,9 +414,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if elapsed_time > self._time_window: if elapsed_time > self._time_window:
derivative = new_derivative derivative = new_derivative
else: else:
derivative = self._calc_derivative_from_state_list( derivative = self._calc_derivative_from_state_list(new_timestamp)
new_state.last_reported
)
self._write_native_value(derivative) self._write_native_value(derivative)
source_state = self.hass.states.get(self._sensor_source_id) source_state = self.hass.states.get(self._sensor_source_id)

View File

@ -9,7 +9,7 @@
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
}, },
"data_description": { "data_description": {
"ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network App on the device dashboard.", "ip_address": "IP address of your devolo Home Network device. This can be found in the devolo Home Network app on the device dashboard.",
"password": "Password you protected the device with." "password": "Password you protected the device with."
} }
}, },
@ -22,8 +22,8 @@
} }
}, },
"zeroconf_confirm": { "zeroconf_confirm": {
"description": "Do you want to add the devolo home network device with the hostname `{host_name}` to Home Assistant?", "description": "Do you want to add the devolo Home Network device with the hostname `{host_name}` to Home Assistant?",
"title": "Discovered devolo home network device", "title": "Discovered devolo Home Network device",
"data": { "data": {
"password": "[%key:common::config_flow::data::password%]" "password": "[%key:common::config_flow::data::password%]"
}, },

View File

@ -6,6 +6,6 @@
"documentation": "https://www.home-assistant.io/integrations/discovergy", "documentation": "https://www.home-assistant.io/integrations/discovergy",
"integration_type": "service", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "quality_scale": "platinum",
"requirements": ["pydiscovergy==3.0.2"] "requirements": ["pydiscovergy==3.0.2"]
} }

View File

@ -57,13 +57,16 @@ rules:
status: exempt status: exempt
comment: | comment: |
This integration cannot be discovered, it is a connecting to a cloud service. This integration cannot be discovered, it is a connecting to a cloud service.
docs-data-update: todo docs-data-update: done
docs-examples: todo docs-examples: done
docs-known-limitations: todo docs-known-limitations:
docs-supported-devices: todo status: exempt
docs-supported-functions: todo comment: |
docs-troubleshooting: todo The integration does not have any known limitations.
docs-use-cases: todo docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: dynamic-devices:
status: exempt status: exempt
comment: | comment: |

View File

@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["async_upnp_client"], "loggers": ["async_upnp_client"],
"requirements": ["async-upnp-client==0.44.0", "getmac==0.9.5"], "requirements": ["async-upnp-client==0.45.0", "getmac==0.9.5"],
"ssdp": [ "ssdp": [
{ {
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@ -7,7 +7,7 @@
"dependencies": ["ssdp"], "dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms", "documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling", "iot_class": "local_polling",
"requirements": ["async-upnp-client==0.44.0"], "requirements": ["async-upnp-client==0.45.0"],
"ssdp": [ "ssdp": [
{ {
"deviceType": "urn:schemas-upnp-org:device:MediaServer:1", "deviceType": "urn:schemas-upnp-org:device:MediaServer:1",

View File

@ -13,15 +13,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up DNS IP from a config entry.""" """Set up DNS IP from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload dnsip config entry.""" """Unload dnsip config entry."""

View File

@ -14,7 +14,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.const import CONF_NAME, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
@ -165,7 +165,7 @@ class DnsIPConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class DnsIPOptionsFlowHandler(OptionsFlow): class DnsIPOptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option config flow for dnsip integration.""" """Handle a option config flow for dnsip integration."""
async def async_step_init( async def async_step_init(

View File

@ -69,16 +69,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> b
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def update_listener(hass: HomeAssistant, entry: EmonCMSConfigEntry):
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: EmonCMSConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_API_KEY, CONF_URL from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import callback from homeassistant.core import callback
@ -221,7 +221,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class EmoncmsOptionsFlow(OptionsFlow): class EmoncmsOptionsFlow(OptionsFlowWithReload):
"""Emoncms Options flow handler.""" """Emoncms Options flow handler."""
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:

View File

@ -295,23 +295,7 @@ class RuntimeEntryData:
needed_platforms.add(Platform.BINARY_SENSOR) needed_platforms.add(Platform.BINARY_SENSOR)
needed_platforms.add(Platform.SELECT) needed_platforms.add(Platform.SELECT)
ent_reg = er.async_get(hass) needed_platforms.update(INFO_TYPE_TO_PLATFORM[type(info)] for info in infos)
registry_get_entity = ent_reg.async_get_entity_id
for info in infos:
platform = INFO_TYPE_TO_PLATFORM[type(info)]
needed_platforms.add(platform)
# If the unique id is in the old format, migrate it
# except if they downgraded and upgraded, there might be a duplicate
# so we want to keep the one that was already there.
if (
(old_unique_id := info.unique_id)
and (old_entry := registry_get_entity(platform, DOMAIN, old_unique_id))
and (new_unique_id := build_device_unique_id(mac, info))
!= old_unique_id
and not registry_get_entity(platform, DOMAIN, new_unique_id)
):
ent_reg.async_update_entity(old_entry, new_unique_id=new_unique_id)
await self._ensure_platforms_loaded(hass, entry, needed_platforms) await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Make a dict of the EntityInfo by type and send # Make a dict of the EntityInfo by type and send

View File

@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"], "mqtt": ["esphome/discover/#"],
"quality_scale": "platinum", "quality_scale": "platinum",
"requirements": [ "requirements": [
"aioesphomeapi==35.0.0", "aioesphomeapi==37.0.2",
"esphome-dashboard-api==1.3.0", "esphome-dashboard-api==1.3.0",
"bleak-esphome==3.1.0" "bleak-esphome==3.1.0"
], ],

View File

@ -94,8 +94,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: EzvizConfigEntry) -> boo
entry.runtime_data = coordinator entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
# Check EZVIZ cloud account entity is present, reload cloud account entities for camera entity change to take effect. # Check EZVIZ cloud account entity is present, reload cloud account entities for camera entity change to take effect.
# Cameras are accessed via local RTSP stream with unique credentials per camera. # Cameras are accessed via local RTSP stream with unique credentials per camera.
# Separate camera entities allow for credential changes per camera. # Separate camera entities allow for credential changes per camera.
@ -120,8 +118,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: EzvizConfigEntry) -> bo
return await hass.config_entries.async_unload_platforms( return await hass.config_entries.async_unload_platforms(
entry, PLATFORMS_BY_TYPE[sensor_type] entry, PLATFORMS_BY_TYPE[sensor_type]
) )
async def _async_update_listener(hass: HomeAssistant, entry: EzvizConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -17,7 +17,11 @@ from pyezvizapi.exceptions import (
from pyezvizapi.test_cam_rtsp import TestRTSPAuth from pyezvizapi.test_cam_rtsp import TestRTSPAuth
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_CUSTOMIZE, CONF_CUSTOMIZE,
CONF_IP_ADDRESS, CONF_IP_ADDRESS,
@ -386,7 +390,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class EzvizOptionsFlowHandler(OptionsFlow): class EzvizOptionsFlowHandler(OptionsFlowWithReload):
"""Handle EZVIZ client options.""" """Handle EZVIZ client options."""
async def async_step_init( async def async_step_init(

View File

@ -32,8 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry) -
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True return True
@ -46,10 +44,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry)
if len(entries) == 1: if len(entries) == 1:
hass.data.pop(MY_KEY) hass.data.pop(MY_KEY)
return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT]) return await hass.config_entries.async_unload_platforms(entry, [Platform.EVENT])
async def _async_update_listener(
hass: HomeAssistant, entry: FeedReaderConfigEntry
) -> None:
"""Handle reconfiguration."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -14,7 +14,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_URL from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -44,7 +44,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
@callback @callback
def async_get_options_flow( def async_get_options_flow(
config_entry: ConfigEntry, config_entry: ConfigEntry,
) -> OptionsFlow: ) -> FeedReaderOptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return FeedReaderOptionsFlowHandler() return FeedReaderOptionsFlowHandler()
@ -119,11 +119,10 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
errors={"base": "url_error"}, errors={"base": "url_error"},
) )
self.hass.config_entries.async_update_entry(reconfigure_entry, data=user_input) return self.async_update_reload_and_abort(reconfigure_entry, data=user_input)
return self.async_abort(reason="reconfigure_successful")
class FeedReaderOptionsFlowHandler(OptionsFlow): class FeedReaderOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow.""" """Handle an options flow."""
async def async_step_init( async def async_step_init(

View File

@ -29,7 +29,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups( await hass.config_entries.async_forward_entry_setups(
entry, [Platform(entry.data[CONF_PLATFORM])] entry, [Platform(entry.data[CONF_PLATFORM])]
) )
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
@ -41,11 +40,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Migrate config entry.""" """Migrate config entry."""
if config_entry.version > 2: if config_entry.version > 2:

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_FILE_PATH, CONF_FILE_PATH,
@ -131,7 +131,7 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._async_handle_step(Platform.SENSOR.value, user_input) return await self._async_handle_step(Platform.SENSOR.value, user_input)
class FileOptionsFlowHandler(OptionsFlow): class FileOptionsFlowHandler(OptionsFlowWithReload):
"""Handle File options.""" """Handle File options."""
async def async_step_init( async def async_step_init(

View File

@ -47,8 +47,6 @@ async def async_setup_entry(
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True return True
@ -57,10 +55,3 @@ async def async_unload_entry(
) -> bool: ) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_update_options(
hass: HomeAssistant, entry: ForecastSolarConfigEntry
) -> None:
"""Update options."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback from homeassistant.core import callback
@ -88,7 +88,7 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
) )
class ForecastSolarOptionFlowHandler(OptionsFlow): class ForecastSolarOptionFlowHandler(OptionsFlowWithReload):
"""Handle options.""" """Handle options."""
async def async_step_init( async def async_step_init(

View File

@ -75,8 +75,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> boo
if FRITZ_DATA_KEY not in hass.data: if FRITZ_DATA_KEY not in hass.data:
hass.data[FRITZ_DATA_KEY] = FritzData() hass.data[FRITZ_DATA_KEY] = FritzData()
entry.async_on_unload(entry.add_update_listener(update_listener))
# Load the other platforms like switch # Load the other platforms like switch
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -94,9 +92,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: FritzConfigEntry) -> bo
hass.data.pop(FRITZ_DATA_KEY) hass.data.pop(FRITZ_DATA_KEY)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: FritzConfigEntry) -> None:
"""Update when config_entry options update."""
if entry.options:
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -17,7 +17,11 @@ from homeassistant.components.device_tracker import (
CONF_CONSIDER_HOME, CONF_CONSIDER_HOME,
DEFAULT_CONSIDER_HOME, DEFAULT_CONSIDER_HOME,
) )
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
@ -409,7 +413,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
) )
class FritzBoxToolsOptionsFlowHandler(OptionsFlow): class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow.""" """Handle an options flow."""
async def async_step_init( async def async_step_init(

View File

@ -48,7 +48,6 @@ async def async_setup_entry(
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
config_entry.runtime_data = fritzbox_phonebook config_entry.runtime_data = fritzbox_phonebook
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True return True
@ -59,10 +58,3 @@ async def async_unload_entry(
) -> bool: ) -> bool:
"""Unloading the fritzbox_callmonitor platforms.""" """Unloading the fritzbox_callmonitor platforms."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def update_listener(
hass: HomeAssistant, config_entry: FritzBoxCallMonitorConfigEntry
) -> None:
"""Update listener to reload after option has changed."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -15,7 +15,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
@ -263,7 +263,7 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlow): class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlowWithReload):
"""Handle a fritzbox_callmonitor options flow.""" """Handle a fritzbox_callmonitor options flow."""
@classmethod @classmethod

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend", "documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system", "integration_type": "system",
"quality_scale": "internal", "quality_scale": "internal",
"requirements": ["home-assistant-frontend==20250702.2"] "requirements": ["home-assistant-frontend==20250702.3"]
} }

View File

@ -13,6 +13,7 @@ from gardena_bluetooth.parse import (
) )
from homeassistant.components.number import ( from homeassistant.components.number import (
NumberDeviceClass,
NumberEntity, NumberEntity,
NumberEntityDescription, NumberEntityDescription,
NumberMode, NumberMode,
@ -54,6 +55,7 @@ DESCRIPTIONS = (
native_step=60, native_step=60,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
char=Valve.manual_watering_time, char=Valve.manual_watering_time,
device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=Valve.remaining_open_time.uuid, key=Valve.remaining_open_time.uuid,
@ -64,6 +66,7 @@ DESCRIPTIONS = (
native_step=60.0, native_step=60.0,
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
char=Valve.remaining_open_time, char=Valve.remaining_open_time,
device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.rain_pause.uuid, key=DeviceConfiguration.rain_pause.uuid,
@ -75,6 +78,7 @@ DESCRIPTIONS = (
native_step=6 * 60.0, native_step=6 * 60.0,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
char=DeviceConfiguration.rain_pause, char=DeviceConfiguration.rain_pause,
device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.seasonal_adjust.uuid, key=DeviceConfiguration.seasonal_adjust.uuid,
@ -86,6 +90,7 @@ DESCRIPTIONS = (
native_step=1.0, native_step=1.0,
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
char=DeviceConfiguration.seasonal_adjust, char=DeviceConfiguration.seasonal_adjust,
device_class=NumberDeviceClass.DURATION,
), ),
GardenaBluetoothNumberEntityDescription( GardenaBluetoothNumberEntityDescription(
key=Sensor.threshold.uuid, key=Sensor.threshold.uuid,
@ -153,6 +158,7 @@ class GardenaBluetoothRemainingOpenSetNumber(GardenaBluetoothEntity, NumberEntit
_attr_native_min_value = 0.0 _attr_native_min_value = 0.0
_attr_native_max_value = 24 * 60 _attr_native_max_value = 24 * 60
_attr_native_step = 1.0 _attr_native_step = 1.0
_attr_device_class = NumberDeviceClass.DURATION
def __init__( def __init__(
self, self,

View File

@ -6,7 +6,11 @@ from typing import Any
from gardena_bluetooth.const import Valve from gardena_bluetooth.const import Valve
from homeassistant.components.valve import ValveEntity, ValveEntityFeature from homeassistant.components.valve import (
ValveDeviceClass,
ValveEntity,
ValveEntityFeature,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -37,6 +41,7 @@ class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity):
_attr_is_closed: bool | None = None _attr_is_closed: bool | None = None
_attr_reports_position = False _attr_reports_position = False
_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
_attr_device_class = ValveDeviceClass.WATER
characteristics = { characteristics = {
Valve.state.uuid, Valve.state.uuid,

View File

@ -47,7 +47,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> bo
async_cleanup_device_registry(hass=hass, entry=entry) async_cleanup_device_registry(hass=hass, entry=entry)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True return True
@ -87,8 +86,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> b
coordinator.unsubscribe() coordinator.unsubscribe()
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_reload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -19,7 +19,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -214,7 +214,7 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler() return OptionsFlowHandler()
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option flow for GitHub.""" """Handle a option flow for GitHub."""
async def async_step_init( async def async_step_init(

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import asdict, fields from dataclasses import asdict, fields
import datetime import datetime
from math import floor from math import floor
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Literal
from dateutil.rrule import ( from dateutil.rrule import (
DAILY, DAILY,
@ -56,7 +56,12 @@ def next_due_date(task: TaskData, today: datetime.datetime) -> datetime.date | N
return dt_util.as_local(task.nextDue[0]).date() return dt_util.as_local(task.nextDue[0]).date()
FREQUENCY_MAP = {"daily": DAILY, "weekly": WEEKLY, "monthly": MONTHLY, "yearly": YEARLY} FREQUENCY_MAP: dict[str, Literal[0, 1, 2, 3]] = {
"daily": DAILY,
"weekly": WEEKLY,
"monthly": MONTHLY,
"yearly": YEARLY,
}
WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU} WEEKDAY_MAP = {"m": MO, "t": TU, "w": WE, "th": TH, "f": FR, "s": SA, "su": SU}

View File

@ -34,16 +34,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_COUNTRY from homeassistant.const import CONF_COUNTRY
from homeassistant.core import callback from homeassistant.core import callback
@ -227,7 +227,7 @@ class HolidayConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class HolidayOptionsFlowHandler(OptionsFlow): class HolidayOptionsFlowHandler(OptionsFlowWithReload):
"""Handle Holiday options.""" """Handle Holiday options."""
async def async_step_init( async def async_step_init(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["homematicip"], "loggers": ["homematicip"],
"requirements": ["homematicip==2.0.7"] "requirements": ["homematicip==2.2.0"]
} }

View File

@ -83,18 +83,9 @@ async def async_setup_entry(
config_entry.runtime_data = HoneywellData(config_entry.entry_id, client, devices) config_entry.runtime_data = HoneywellData(config_entry.entry_id, client, devices)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
return True return True
async def update_listener(
hass: HomeAssistant, config_entry: HoneywellConfigEntry
) -> None:
"""Update listener."""
await hass.config_entries.async_reload(config_entry.entry_id)
async def async_unload_entry( async def async_unload_entry(
hass: HomeAssistant, config_entry: HoneywellConfigEntry hass: HomeAssistant, config_entry: HoneywellConfigEntry
) -> bool: ) -> bool:

View File

@ -12,7 +12,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback from homeassistant.core import callback
@ -136,7 +136,7 @@ class HoneywellConfigFlow(ConfigFlow, domain=DOMAIN):
return HoneywellOptionsFlowHandler() return HoneywellOptionsFlowHandler()
class HoneywellOptionsFlowHandler(OptionsFlow): class HoneywellOptionsFlowHandler(OptionsFlowWithReload):
"""Config flow options for Honeywell.""" """Config flow options for Honeywell."""
async def async_step_init(self, user_input=None) -> ConfigFlowResult: async def async_step_init(self, user_input=None) -> ConfigFlowResult:

View File

@ -70,6 +70,8 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
@property @property
def event(self) -> CalendarEvent | None: def event(self) -> CalendarEvent | None:
"""Return the current or next upcoming event.""" """Return the current or next upcoming event."""
if not self.available:
return None
schedule = self.mower_attributes.calendar schedule = self.mower_attributes.calendar
cursor = schedule.timeline.active_after(dt_util.now()) cursor = schedule.timeline.active_after(dt_util.now())
program_event = next(cursor, None) program_event = next(cursor, None)
@ -94,6 +96,8 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
This is only called when opening the calendar in the UI. This is only called when opening the calendar in the UI.
""" """
if not self.available:
return []
schedule = self.mower_attributes.calendar schedule = self.mower_attributes.calendar
cursor = schedule.timeline.overlapping( cursor = schedule.timeline.overlapping(
start_date, start_date,

View File

@ -6,6 +6,7 @@ import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import override
from aioautomower.exceptions import ( from aioautomower.exceptions import (
ApiError, ApiError,
@ -60,7 +61,12 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
self._devices_last_update: set[str] = set() self._devices_last_update: set[str] = set()
self._zones_last_update: dict[str, set[str]] = {} self._zones_last_update: dict[str, set[str]] = {}
self._areas_last_update: dict[str, set[int]] = {} self._areas_last_update: dict[str, set[int]] = {}
self.async_add_listener(self._on_data_update)
@override
@callback
def async_update_listeners(self) -> None:
self._on_data_update()
super().async_update_listeners()
async def _async_update_data(self) -> MowerDictionary: async def _async_update_data(self) -> MowerDictionary:
"""Subscribe for websocket and poll data from the API.""" """Subscribe for websocket and poll data from the API."""

View File

@ -114,6 +114,11 @@ class AutomowerBaseEntity(CoordinatorEntity[AutomowerDataUpdateCoordinator]):
"""Get the mower attributes of the current mower.""" """Get the mower attributes of the current mower."""
return self.coordinator.data[self.mower_id] return self.coordinator.data[self.mower_id]
@property
def available(self) -> bool:
"""Return True if the device is available."""
return super().available and self.mower_id in self.coordinator.data
class AutomowerAvailableEntity(AutomowerBaseEntity): class AutomowerAvailableEntity(AutomowerBaseEntity):
"""Replies available when the mower is connected.""" """Replies available when the mower is connected."""

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["aioautomower"], "loggers": ["aioautomower"],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["aioautomower==1.2.2"] "requirements": ["aioautomower==2.0.0"]
} }

View File

@ -541,6 +541,11 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
"""Return the state attributes.""" """Return the state attributes."""
return self.entity_description.extra_state_attributes_fn(self.mower_attributes) return self.entity_description.extra_state_attributes_fn(self.mower_attributes)
@property
def available(self) -> bool:
"""Return the available attribute of the entity."""
return super().available and self.native_value is not None
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity): class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
"""Defining the Work area sensors with WorkAreaSensorEntityDescription.""" """Defining the Work area sensors with WorkAreaSensorEntityDescription."""

View File

@ -2,46 +2,28 @@
from __future__ import annotations from __future__ import annotations
import logging
from huum.exceptions import Forbidden, NotAuthenticated
from huum.huum import Huum
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, PLATFORMS from .const import PLATFORMS
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, config_entry: HuumConfigEntry) -> bool:
"""Set up Huum from a config entry.""" """Set up Huum from a config entry."""
username = entry.data[CONF_USERNAME] coordinator = HuumDataUpdateCoordinator(
password = entry.data[CONF_PASSWORD] hass=hass,
config_entry=config_entry,
)
huum = Huum(username, password, session=async_get_clientsession(hass)) await coordinator.async_config_entry_first_refresh()
config_entry.runtime_data = coordinator
try: await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
await huum.status()
except (Forbidden, NotAuthenticated) as err:
_LOGGER.error("Could not log in to Huum with given credentials")
raise ConfigEntryNotReady(
"Could not log in to Huum with given credentials"
) from err
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = huum
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(
hass: HomeAssistant, config_entry: HuumConfigEntry
) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok

View File

@ -0,0 +1,42 @@
"""Sensor for door state."""
from __future__ import annotations
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
from .entity import HuumBaseEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: HuumConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up door sensor."""
async_add_entities(
[HuumDoorSensor(config_entry.runtime_data)],
)
class HuumDoorSensor(HuumBaseEntity, BinarySensorEntity):
"""Representation of a BinarySensor."""
_attr_name = "Door"
_attr_device_class = BinarySensorDeviceClass.DOOR
def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
"""Initialize the BinarySensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_door"
@property
def is_on(self) -> bool | None:
"""Return the current value."""
return not self.coordinator.data.door_closed

View File

@ -7,38 +7,33 @@ from typing import Any
from huum.const import SaunaStatus from huum.const import SaunaStatus
from huum.exceptions import SafetyException from huum.exceptions import SafetyException
from huum.huum import Huum
from huum.schemas import HuumStatusResponse
from homeassistant.components.climate import ( from homeassistant.components.climate import (
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
HVACMode, HVACMode,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
from .entity import HuumBaseEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
entry: ConfigEntry, entry: HuumConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback, async_add_entities: AddConfigEntryEntitiesCallback,
) -> None: ) -> None:
"""Set up the Huum sauna with config flow.""" """Set up the Huum sauna with config flow."""
huum_handler = hass.data.setdefault(DOMAIN, {})[entry.entry_id] async_add_entities([HuumDevice(entry.runtime_data)])
async_add_entities([HuumDevice(huum_handler, entry.entry_id)], True)
class HuumDevice(ClimateEntity): class HuumDevice(HuumBaseEntity, ClimateEntity):
"""Representation of a heater.""" """Representation of a heater."""
_attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] _attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF]
@ -49,29 +44,28 @@ class HuumDevice(ClimateEntity):
) )
_attr_target_temperature_step = PRECISION_WHOLE _attr_target_temperature_step = PRECISION_WHOLE
_attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_max_temp = 110
_attr_min_temp = 40
_attr_has_entity_name = True
_attr_name = None _attr_name = None
_target_temperature: int | None = None def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
_status: HuumStatusResponse | None = None
def __init__(self, huum_handler: Huum, unique_id: str) -> None:
"""Initialize the heater.""" """Initialize the heater."""
self._attr_unique_id = unique_id super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name="Huum sauna",
manufacturer="Huum",
)
self._huum_handler = huum_handler self._attr_unique_id = coordinator.config_entry.entry_id
@property
def min_temp(self) -> int:
"""Return configured minimal temperature."""
return self.coordinator.data.sauna_config.min_temp
@property
def max_temp(self) -> int:
"""Return configured maximum temperature."""
return self.coordinator.data.sauna_config.max_temp
@property @property
def hvac_mode(self) -> HVACMode: def hvac_mode(self) -> HVACMode:
"""Return hvac operation ie. heat, cool mode.""" """Return hvac operation ie. heat, cool mode."""
if self._status and self._status.status == SaunaStatus.ONLINE_HEATING: if self.coordinator.data.status == SaunaStatus.ONLINE_HEATING:
return HVACMode.HEAT return HVACMode.HEAT
return HVACMode.OFF return HVACMode.OFF
@ -85,41 +79,33 @@ class HuumDevice(ClimateEntity):
@property @property
def current_temperature(self) -> int | None: def current_temperature(self) -> int | None:
"""Return the current temperature.""" """Return the current temperature."""
if (status := self._status) is not None: return self.coordinator.data.temperature
return status.temperature
return None
@property @property
def target_temperature(self) -> int: def target_temperature(self) -> int:
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
return self._target_temperature or int(self.min_temp) return self.coordinator.data.target_temperature or int(self.min_temp)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set hvac mode.""" """Set hvac mode."""
if hvac_mode == HVACMode.HEAT: if hvac_mode == HVACMode.HEAT:
await self._turn_on(self.target_temperature) await self._turn_on(self.target_temperature)
elif hvac_mode == HVACMode.OFF: elif hvac_mode == HVACMode.OFF:
await self._huum_handler.turn_off() await self.coordinator.huum.turn_off()
await self.coordinator.async_refresh()
async def async_set_temperature(self, **kwargs: Any) -> None: async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature.""" """Set new target temperature."""
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
if temperature is None: if temperature is None or self.hvac_mode != HVACMode.HEAT:
return return
self._target_temperature = temperature
if self.hvac_mode == HVACMode.HEAT: await self._turn_on(temperature)
await self._turn_on(temperature) await self.coordinator.async_refresh()
async def async_update(self) -> None:
"""Get the latest status data."""
self._status = await self._huum_handler.status()
if self._target_temperature is None or self.hvac_mode == HVACMode.HEAT:
self._target_temperature = self._status.target_temperature
async def _turn_on(self, temperature: int) -> None: async def _turn_on(self, temperature: int) -> None:
try: try:
await self._huum_handler.turn_on(temperature) await self.coordinator.huum.turn_on(temperature)
except (ValueError, SafetyException) as err: except (ValueError, SafetyException) as err:
_LOGGER.error(str(err)) _LOGGER.error(str(err))
raise HomeAssistantError(f"Unable to turn on sauna: {err}") from err raise HomeAssistantError(f"Unable to turn on sauna: {err}") from err

View File

@ -37,12 +37,12 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {} errors = {}
if user_input is not None: if user_input is not None:
try: try:
huum_handler = Huum( huum = Huum(
user_input[CONF_USERNAME], user_input[CONF_USERNAME],
user_input[CONF_PASSWORD], user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass), session=async_get_clientsession(self.hass),
) )
await huum_handler.status() await huum.status()
except (Forbidden, NotAuthenticated): except (Forbidden, NotAuthenticated):
# Most likely Forbidden as that is what is returned from `.status()` with bad creds # Most likely Forbidden as that is what is returned from `.status()` with bad creds
_LOGGER.error("Could not log in to Huum with given credentials") _LOGGER.error("Could not log in to Huum with given credentials")

View File

@ -4,4 +4,4 @@ from homeassistant.const import Platform
DOMAIN = "huum" DOMAIN = "huum"
PLATFORMS = [Platform.CLIMATE] PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE]

View File

@ -0,0 +1,60 @@
"""DataUpdateCoordinator for Huum."""
from __future__ import annotations
from datetime import timedelta
import logging
from huum.exceptions import Forbidden, NotAuthenticated
from huum.huum import Huum
from huum.schemas import HuumStatusResponse
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
type HuumConfigEntry = ConfigEntry[HuumDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=30)
class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
"""Class to manage fetching data from the API."""
config_entry: HuumConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: HuumConfigEntry,
) -> None:
"""Initialize."""
super().__init__(
hass=hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
config_entry=config_entry,
)
self.huum = Huum(
config_entry.data[CONF_USERNAME],
config_entry.data[CONF_PASSWORD],
session=async_get_clientsession(hass),
)
async def _async_update_data(self) -> HuumStatusResponse:
"""Get the latest status data."""
try:
return await self.huum.status()
except (Forbidden, NotAuthenticated) as err:
_LOGGER.error("Could not log in to Huum with given credentials")
raise UpdateFailed(
"Could not log in to Huum with given credentials"
) from err

View File

@ -0,0 +1,24 @@
"""Define Huum Base entity."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import HuumDataUpdateCoordinator
class HuumBaseEntity(CoordinatorEntity[HuumDataUpdateCoordinator]):
"""Huum base Entity."""
_attr_has_entity_name = True
def __init__(self, coordinator: HuumDataUpdateCoordinator) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
name="Huum sauna",
manufacturer="Huum",
model="UKU WiFi",
)

View File

@ -1,7 +1,7 @@
{ {
"domain": "huum", "domain": "huum",
"name": "Huum", "name": "Huum",
"codeowners": ["@frwickst"], "codeowners": ["@frwickst", "@vincentwolsink"],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huum", "documentation": "https://www.home-assistant.io/integrations/huum",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib", "documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"quality_scale": "silver", "quality_scale": "silver",
"requirements": ["imgw_pib==1.4.1"] "requirements": ["imgw_pib==1.4.2"]
} }

View File

@ -15,6 +15,7 @@ from homeassistant.const import (
CONF_MODE, CONF_MODE,
CONF_NAME, CONF_NAME,
CONF_UNIT_OF_MEASUREMENT, CONF_UNIT_OF_MEASUREMENT,
MAX_LENGTH_STATE_STATE,
SERVICE_RELOAD, SERVICE_RELOAD,
) )
from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.core import HomeAssistant, ServiceCall, callback
@ -51,8 +52,12 @@ STORAGE_VERSION = 1
STORAGE_FIELDS: VolDictType = { STORAGE_FIELDS: VolDictType = {
vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)), vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.All(
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Coerce(int), vol.Range(0, MAX_LENGTH_STATE_STATE)
),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.All(
vol.Coerce(int), vol.Range(1, MAX_LENGTH_STATE_STATE)
),
vol.Optional(CONF_INITIAL, ""): cv.string, vol.Optional(CONF_INITIAL, ""): cv.string,
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
@ -84,8 +89,12 @@ CONFIG_SCHEMA = vol.Schema(
lambda value: value or {}, lambda value: value or {},
{ {
vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.All(
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), vol.Coerce(int), vol.Range(0, MAX_LENGTH_STATE_STATE)
),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.All(
vol.Coerce(int), vol.Range(1, MAX_LENGTH_STATE_STATE)
),
vol.Optional(CONF_INITIAL): cv.string, vol.Optional(CONF_INITIAL): cv.string,
vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,

View File

@ -463,7 +463,7 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state update when sub interval is configured.""" """Handle sensor state update when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval( self._integrate_on_state_update_with_max_sub_interval(
None, event.data["old_state"], event.data["new_state"] None, None, event.data["old_state"], event.data["new_state"]
) )
@callback @callback
@ -472,13 +472,17 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state report when sub interval is configured.""" """Handle sensor state report when sub interval is configured."""
self._integrate_on_state_update_with_max_sub_interval( self._integrate_on_state_update_with_max_sub_interval(
event.data["old_last_reported"], None, event.data["new_state"] event.data["old_last_reported"],
event.data["last_reported"],
None,
event.data["new_state"],
) )
@callback @callback
def _integrate_on_state_update_with_max_sub_interval( def _integrate_on_state_update_with_max_sub_interval(
self, self,
old_last_reported: datetime | None, old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
) -> None: ) -> None:
@ -489,7 +493,9 @@ class IntegrationSensor(RestoreSensor):
""" """
self._cancel_max_sub_interval_exceeded_callback() self._cancel_max_sub_interval_exceeded_callback()
try: try:
self._integrate_on_state_change(old_last_reported, old_state, new_state) self._integrate_on_state_change(
old_timestamp, new_timestamp, old_state, new_state
)
self._last_integration_trigger = _IntegrationTrigger.StateEvent self._last_integration_trigger = _IntegrationTrigger.StateEvent
self._last_integration_time = datetime.now(tz=UTC) self._last_integration_time = datetime.now(tz=UTC)
finally: finally:
@ -503,7 +509,7 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state change.""" """Handle sensor state change."""
return self._integrate_on_state_change( return self._integrate_on_state_change(
None, event.data["old_state"], event.data["new_state"] None, None, event.data["old_state"], event.data["new_state"]
) )
@callback @callback
@ -512,12 +518,16 @@ class IntegrationSensor(RestoreSensor):
) -> None: ) -> None:
"""Handle sensor state report.""" """Handle sensor state report."""
return self._integrate_on_state_change( return self._integrate_on_state_change(
event.data["old_last_reported"], None, event.data["new_state"] event.data["old_last_reported"],
event.data["last_reported"],
None,
event.data["new_state"],
) )
def _integrate_on_state_change( def _integrate_on_state_change(
self, self,
old_last_reported: datetime | None, old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None, old_state: State | None,
new_state: State | None, new_state: State | None,
) -> None: ) -> None:
@ -531,16 +541,17 @@ class IntegrationSensor(RestoreSensor):
if old_state: if old_state:
# state has changed, we recover old_state from the event # state has changed, we recover old_state from the event
new_timestamp = new_state.last_updated
old_state_state = old_state.state old_state_state = old_state.state
old_last_reported = old_state.last_reported old_timestamp = old_state.last_reported
else: else:
# event state reported without any state change # first state or event state reported without any state change
old_state_state = new_state.state old_state_state = new_state.state
self._attr_available = True self._attr_available = True
self._derive_and_set_attributes_from_state(new_state) self._derive_and_set_attributes_from_state(new_state)
if old_last_reported is None and old_state is None: if old_timestamp is None and old_state is None:
self.async_write_ha_state() self.async_write_ha_state()
return return
@ -551,11 +562,12 @@ class IntegrationSensor(RestoreSensor):
return return
if TYPE_CHECKING: if TYPE_CHECKING:
assert old_last_reported is not None assert new_timestamp is not None
assert old_timestamp is not None
elapsed_seconds = Decimal( elapsed_seconds = Decimal(
(new_state.last_reported - old_last_reported).total_seconds() (new_timestamp - old_timestamp).total_seconds()
if self._last_integration_trigger == _IntegrationTrigger.StateEvent if self._last_integration_trigger == _IntegrationTrigger.StateEvent
else (new_state.last_reported - self._last_integration_time).total_seconds() else (new_timestamp - self._last_integration_time).total_seconds()
) )
area = self._method.calculate_area_with_two_states(elapsed_seconds, *states) area = self._method.calculate_area_with_two_states(elapsed_seconds, *states)

View File

@ -171,7 +171,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
_LOGGER.debug("ISY Starting Event Stream and automatic updates") _LOGGER.debug("ISY Starting Event Stream and automatic updates")
isy.websocket.start() isy.websocket.start()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop_auto_update)
) )
@ -179,11 +178,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: IsyConfigEntry) -> bool:
return True return True
async def _async_update_listener(hass: HomeAssistant, entry: IsyConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
@callback @callback
def _async_get_or_create_isy_device_in_registry( def _async_get_or_create_isy_device_in_registry(
hass: HomeAssistant, entry: IsyConfigEntry, isy: ISY hass: HomeAssistant, entry: IsyConfigEntry, isy: ISY

View File

@ -18,7 +18,7 @@ from homeassistant.config_entries import (
SOURCE_IGNORE, SOURCE_IGNORE,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
@ -143,7 +143,7 @@ class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN):
@callback @callback
def async_get_options_flow( def async_get_options_flow(
config_entry: IsyConfigEntry, config_entry: IsyConfigEntry,
) -> OptionsFlow: ) -> OptionsFlowHandler:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler() return OptionsFlowHandler()
@ -316,7 +316,7 @@ class Isy994ConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option flow for ISY/IoX.""" """Handle a option flow for ISY/IoX."""
async def async_step_init( async def async_step_init(

View File

@ -79,13 +79,6 @@ async def async_setup_entry(
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
async def update_listener(
hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
) -> None:
# Trigger update of states for all platforms
await hass.config_entries.async_reload(config_entry.entry_id)
config_entry.async_on_unload(config_entry.add_update_listener(update_listener))
return True return True

View File

@ -9,7 +9,11 @@ import zoneinfo
from hdate.translator import Language from hdate.translator import Language
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_ELEVATION, CONF_ELEVATION,
CONF_LANGUAGE, CONF_LANGUAGE,
@ -124,7 +128,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(reconfigure_entry, data=user_input) return self.async_update_reload_and_abort(reconfigure_entry, data=user_input)
class JewishCalendarOptionsFlowHandler(OptionsFlow): class JewishCalendarOptionsFlowHandler(OptionsFlowWithReload):
"""Handle Jewish Calendar options.""" """Handle Jewish Calendar options."""
async def async_step_init( async def async_step_init(

View File

@ -33,8 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) ->
router = KeeneticRouter(hass, entry) router = KeeneticRouter(hass, entry)
await router.async_setup() await router.async_setup()
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.runtime_data = router entry.runtime_data = router
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -87,11 +85,6 @@ async def async_unload_entry(
return unload_ok return unload_ok
async def update_listener(hass: HomeAssistant, entry: KeeneticConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry): def async_add_defaults(hass: HomeAssistant, entry: KeeneticConfigEntry):
"""Populate default options.""" """Populate default options."""
host: str = entry.data[CONF_HOST] host: str = entry.data[CONF_HOST]

View File

@ -12,7 +12,7 @@ from homeassistant.config_entries import (
SOURCE_RECONFIGURE, SOURCE_RECONFIGURE,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_HOST, CONF_HOST,
@ -153,7 +153,7 @@ class KeeneticFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_user() return await self.async_step_user()
class KeeneticOptionsFlowHandler(OptionsFlow): class KeeneticOptionsFlowHandler(OptionsFlowWithReload):
"""Handle options.""" """Handle options."""
config_entry: KeeneticConfigEntry config_entry: KeeneticConfigEntry

View File

@ -101,19 +101,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Start a reauth flow # Start a reauth flow
entry.async_start_reauth(hass) entry.async_start_reauth(hass)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
# Notify backup listeners # Notify backup listeners
hass.async_create_task(_notify_backup_listeners(hass), eager_start=False) hass.async_create_task(_notify_backup_listeners(hass), eager_start=False)
return True return True
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload config entry.""" """Unload config entry."""
# Notify backup listeners # Notify backup listeners

View File

@ -13,7 +13,7 @@ from homeassistant.config_entries import (
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
ConfigSubentryFlow, ConfigSubentryFlow,
OptionsFlow, OptionsFlowWithReload,
SubentryFlowResult, SubentryFlowResult,
) )
from homeassistant.core import callback from homeassistant.core import callback
@ -65,7 +65,7 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
class OptionsFlowHandler(OptionsFlow): class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle options.""" """Handle options."""
async def async_step_init( async def async_step_init(

View File

@ -120,8 +120,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.data[KNX_MODULE_KEY] = knx_module hass.data[KNX_MODULE_KEY] = knx_module
entry.async_on_unload(entry.add_update_listener(async_update_entry))
if CONF_KNX_EXPOSE in config: if CONF_KNX_EXPOSE in config:
for expose_config in config[CONF_KNX_EXPOSE]: for expose_config in config[CONF_KNX_EXPOSE]:
knx_module.exposures.append( knx_module.exposures.append(
@ -174,11 +172,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return unload_ok return unload_ok
async def async_update_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update a given config entry."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Remove a config entry.""" """Remove a config entry."""

View File

@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback from homeassistant.core import callback
@ -899,7 +899,7 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
) )
class KNXOptionsFlow(OptionsFlow): class KNXOptionsFlow(OptionsFlowWithReload):
"""Handle KNX options.""" """Handle KNX options."""
def __init__(self, config_entry: ConfigEntry) -> None: def __init__(self, config_entry: ConfigEntry) -> None:

View File

@ -154,13 +154,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) -
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def update_listener(
hass: HomeAssistant, entry: LaMarzoccoConfigEntry
) -> None:
await hass.config_entries.async_reload(entry.entry_id)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True

View File

@ -21,7 +21,7 @@ from homeassistant.config_entries import (
SOURCE_RECONFIGURE, SOURCE_RECONFIGURE,
ConfigFlow, ConfigFlow,
ConfigFlowResult, ConfigFlowResult,
OptionsFlow, OptionsFlowWithReload,
) )
from homeassistant.const import ( from homeassistant.const import (
CONF_ADDRESS, CONF_ADDRESS,
@ -363,7 +363,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
return LmOptionsFlowHandler() return LmOptionsFlowHandler()
class LmOptionsFlowHandler(OptionsFlow): class LmOptionsFlowHandler(OptionsFlowWithReload):
"""Handles options flow for the component.""" """Handles options flow for the component."""
async def async_step_init( async def async_step_init(

View File

@ -16,7 +16,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: LastFMConfigEntry) -> bo
entry.runtime_data = coordinator entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True return True
@ -24,8 +23,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: LastFMConfigEntry) -> bo
async def async_unload_entry(hass: HomeAssistant, entry: LastFMConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: LastFMConfigEntry) -> bool:
"""Unload lastfm config entry.""" """Unload lastfm config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: LastFMConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@ -8,7 +8,11 @@ from typing import Any
from pylast import LastFMNetwork, PyLastError, User, WSError from pylast import LastFMNetwork, PyLastError, User, WSError
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
@ -155,7 +159,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
) )
class LastFmOptionsFlowHandler(OptionsFlow): class LastFmOptionsFlowHandler(OptionsFlowWithReload):
"""LastFm Options flow handler.""" """LastFm Options flow handler."""
config_entry: LastFMConfigEntry config_entry: LastFMConfigEntry

View File

@ -6,6 +6,7 @@ import asyncio
from letpot.client import LetPotClient from letpot.client import LetPotClient
from letpot.converters import CONVERTERS from letpot.converters import CONVERTERS
from letpot.deviceclient import LetPotDeviceClient
from letpot.exceptions import LetPotAuthenticationException, LetPotException from letpot.exceptions import LetPotAuthenticationException, LetPotException
from letpot.models import AuthenticationInfo from letpot.models import AuthenticationInfo
@ -68,8 +69,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bo
except LetPotException as exc: except LetPotException as exc:
raise ConfigEntryNotReady from exc raise ConfigEntryNotReady from exc
device_client = LetPotDeviceClient(auth)
coordinators: list[LetPotDeviceCoordinator] = [ coordinators: list[LetPotDeviceCoordinator] = [
LetPotDeviceCoordinator(hass, entry, auth, device) LetPotDeviceCoordinator(hass, entry, device, device_client)
for device in devices for device in devices
if any(converter.supports_type(device.device_type) for converter in CONVERTERS) if any(converter.supports_type(device.device_type) for converter in CONVERTERS)
] ]
@ -92,5 +95,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> b
"""Unload a config entry.""" """Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
for coordinator in entry.runtime_data: for coordinator in entry.runtime_data:
coordinator.device_client.disconnect() await coordinator.device_client.unsubscribe(
coordinator.device.serial_number
)
return unload_ok return unload_ok

View File

@ -58,7 +58,9 @@ BINARY_SENSORS: tuple[LetPotBinarySensorEntityDescription, ...] = (
device_class=BinarySensorDeviceClass.RUNNING, device_class=BinarySensorDeviceClass.RUNNING,
supported_fn=( supported_fn=(
lambda coordinator: DeviceFeature.PUMP_STATUS lambda coordinator: DeviceFeature.PUMP_STATUS
in coordinator.device_client.device_features in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
), ),
), ),
LetPotBinarySensorEntityDescription( LetPotBinarySensorEntityDescription(

View File

@ -8,7 +8,7 @@ import logging
from letpot.deviceclient import LetPotDeviceClient from letpot.deviceclient import LetPotDeviceClient
from letpot.exceptions import LetPotAuthenticationException, LetPotException from letpot.exceptions import LetPotAuthenticationException, LetPotException
from letpot.models import AuthenticationInfo, LetPotDevice, LetPotDeviceStatus from letpot.models import LetPotDevice, LetPotDeviceStatus
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -34,8 +34,8 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
self, self,
hass: HomeAssistant, hass: HomeAssistant,
config_entry: LetPotConfigEntry, config_entry: LetPotConfigEntry,
info: AuthenticationInfo,
device: LetPotDevice, device: LetPotDevice,
device_client: LetPotDeviceClient,
) -> None: ) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
super().__init__( super().__init__(
@ -45,9 +45,8 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
name=f"LetPot {device.serial_number}", name=f"LetPot {device.serial_number}",
update_interval=timedelta(minutes=10), update_interval=timedelta(minutes=10),
) )
self._info = info
self.device = device self.device = device
self.device_client = LetPotDeviceClient(info, device.serial_number) self.device_client = device_client
def _handle_status_update(self, status: LetPotDeviceStatus) -> None: def _handle_status_update(self, status: LetPotDeviceStatus) -> None:
"""Distribute status update to entities.""" """Distribute status update to entities."""
@ -56,7 +55,9 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
async def _async_setup(self) -> None: async def _async_setup(self) -> None:
"""Set up subscription for coordinator.""" """Set up subscription for coordinator."""
try: try:
await self.device_client.subscribe(self._handle_status_update) await self.device_client.subscribe(
self.device.serial_number, self._handle_status_update
)
except LetPotAuthenticationException as exc: except LetPotAuthenticationException as exc:
raise ConfigEntryAuthFailed from exc raise ConfigEntryAuthFailed from exc
@ -64,7 +65,7 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
"""Request an update from the device and wait for a status update or timeout.""" """Request an update from the device and wait for a status update or timeout."""
try: try:
async with asyncio.timeout(REQUEST_UPDATE_TIMEOUT): async with asyncio.timeout(REQUEST_UPDATE_TIMEOUT):
await self.device_client.get_current_status() await self.device_client.get_current_status(self.device.serial_number)
except LetPotException as exc: except LetPotException as exc:
raise UpdateFailed(exc) from exc raise UpdateFailed(exc) from exc

View File

@ -30,12 +30,13 @@ class LetPotEntity(CoordinatorEntity[LetPotDeviceCoordinator]):
def __init__(self, coordinator: LetPotDeviceCoordinator) -> None: def __init__(self, coordinator: LetPotDeviceCoordinator) -> None:
"""Initialize a LetPot entity.""" """Initialize a LetPot entity."""
super().__init__(coordinator) super().__init__(coordinator)
info = coordinator.device_client.device_info(coordinator.device.serial_number)
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, coordinator.device.serial_number)}, identifiers={(DOMAIN, coordinator.device.serial_number)},
name=coordinator.device.name, name=coordinator.device.name,
manufacturer="LetPot", manufacturer="LetPot",
model=coordinator.device_client.device_model_name, model=info.model_name,
model_id=coordinator.device_client.device_model_code, model_id=info.model_code,
serial_number=coordinator.device.serial_number, serial_number=coordinator.device.serial_number,
) )

View File

@ -6,6 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/letpot", "documentation": "https://www.home-assistant.io/integrations/letpot",
"integration_type": "hub", "integration_type": "hub",
"iot_class": "cloud_push", "iot_class": "cloud_push",
"loggers": ["letpot"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["letpot==0.4.0"] "requirements": ["letpot==0.5.0"]
} }

View File

@ -50,7 +50,9 @@ SENSORS: tuple[LetPotSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
supported_fn=( supported_fn=(
lambda coordinator: DeviceFeature.TEMPERATURE lambda coordinator: DeviceFeature.TEMPERATURE
in coordinator.device_client.device_features in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
), ),
), ),
LetPotSensorEntityDescription( LetPotSensorEntityDescription(
@ -61,7 +63,9 @@ SENSORS: tuple[LetPotSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
supported_fn=( supported_fn=(
lambda coordinator: DeviceFeature.WATER_LEVEL lambda coordinator: DeviceFeature.WATER_LEVEL
in coordinator.device_client.device_features in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
), ),
), ),
) )

View File

@ -25,7 +25,7 @@ class LetPotSwitchEntityDescription(LetPotEntityDescription, SwitchEntityDescrip
"""Describes a LetPot switch entity.""" """Describes a LetPot switch entity."""
value_fn: Callable[[LetPotDeviceStatus], bool | None] value_fn: Callable[[LetPotDeviceStatus], bool | None]
set_value_fn: Callable[[LetPotDeviceClient, bool], Coroutine[Any, Any, None]] set_value_fn: Callable[[LetPotDeviceClient, str, bool], Coroutine[Any, Any, None]]
SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = ( SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
@ -33,7 +33,9 @@ SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
key="alarm_sound", key="alarm_sound",
translation_key="alarm_sound", translation_key="alarm_sound",
value_fn=lambda status: status.system_sound, value_fn=lambda status: status.system_sound,
set_value_fn=lambda device_client, value: device_client.set_sound(value), set_value_fn=(
lambda device_client, serial, value: device_client.set_sound(serial, value)
),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
supported_fn=lambda coordinator: coordinator.data.system_sound is not None, supported_fn=lambda coordinator: coordinator.data.system_sound is not None,
), ),
@ -41,25 +43,35 @@ SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
key="auto_mode", key="auto_mode",
translation_key="auto_mode", translation_key="auto_mode",
value_fn=lambda status: status.water_mode == 1, value_fn=lambda status: status.water_mode == 1,
set_value_fn=lambda device_client, value: device_client.set_water_mode(value), set_value_fn=(
lambda device_client, serial, value: device_client.set_water_mode(
serial, value
)
),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
supported_fn=( supported_fn=(
lambda coordinator: DeviceFeature.PUMP_AUTO lambda coordinator: DeviceFeature.PUMP_AUTO
in coordinator.device_client.device_features in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
), ),
), ),
LetPotSwitchEntityDescription( LetPotSwitchEntityDescription(
key="power", key="power",
translation_key="power", translation_key="power",
value_fn=lambda status: status.system_on, value_fn=lambda status: status.system_on,
set_value_fn=lambda device_client, value: device_client.set_power(value), set_value_fn=lambda device_client, serial, value: device_client.set_power(
serial, value
),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
), ),
LetPotSwitchEntityDescription( LetPotSwitchEntityDescription(
key="pump_cycling", key="pump_cycling",
translation_key="pump_cycling", translation_key="pump_cycling",
value_fn=lambda status: status.pump_mode == 1, value_fn=lambda status: status.pump_mode == 1,
set_value_fn=lambda device_client, value: device_client.set_pump_mode(value), set_value_fn=lambda device_client, serial, value: device_client.set_pump_mode(
serial, value
),
entity_category=EntityCategory.CONFIG, entity_category=EntityCategory.CONFIG,
), ),
) )
@ -104,11 +116,13 @@ class LetPotSwitchEntity(LetPotEntity, SwitchEntity):
@exception_handler @exception_handler
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
await self.entity_description.set_value_fn(self.coordinator.device_client, True) await self.entity_description.set_value_fn(
self.coordinator.device_client, self.coordinator.device.serial_number, True
)
@exception_handler @exception_handler
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""
await self.entity_description.set_value_fn( await self.entity_description.set_value_fn(
self.coordinator.device_client, False self.coordinator.device_client, self.coordinator.device.serial_number, False
) )

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