Merge branch 'dev' into state_selector_climate_hvac_mode

This commit is contained in:
Paul Bottein 2025-07-21 14:19:08 +02:00 committed by GitHub
commit 1d2dccf259
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.
## 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
- **Compatibility**: Python 3.13+

View File

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

View File

@ -231,7 +231,7 @@ jobs:
- name: Detect duplicates using AI
id: ai_detection
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:
model: openai/gpt-4o
system-prompt: |

View File

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

4
CODEOWNERS generated
View File

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

View File

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

View File

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

View File

@ -45,9 +45,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: AirNowConfigEntry) -> bo
# Store Entity and Initialize Platforms
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)
# 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:
"""Unload a config entry."""
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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS
from homeassistant.core import HomeAssistant, callback
@ -126,7 +126,7 @@ class AirNowConfigFlow(ConfigFlow, domain=DOMAIN):
return AirNowOptionsFlowHandler()
class AirNowOptionsFlowHandler(OptionsFlow):
class AirNowOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow for AirNow."""
async def async_step_init(

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
"iot_class": "cloud_push",
"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",
"loggers": ["aioamazondevices"],
"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)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
return True
@ -65,10 +64,3 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
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
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.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
@ -129,7 +133,7 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
)
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlow):
class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithReload):
"""Handle Homeassistant Analytics options."""
async def async_step_init(

View File

@ -68,7 +68,6 @@ async def async_setup_entry(
entry.async_on_unload(
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)
return True
@ -80,13 +79,3 @@ async def async_unload_entry(
"""Unload a config entry."""
_LOGGER.debug("async_unload_entry: %s", entry.data)
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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME
from homeassistant.core import callback
@ -116,10 +116,10 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
pin = user_input["pin"]
await self.api.async_finish_pairing(pin)
if self.source == SOURCE_REAUTH:
await self.hass.config_entries.async_reload(
self._get_reauth_entry().entry_id
return self.async_update_reload_and_abort(
self._get_reauth_entry(), reload_even_if_entry_is_unchanged=True
)
return self.async_abort(reason="reauth_successful")
return self.async_create_entry(
title=self.name,
data={
@ -243,7 +243,7 @@ class AndroidTVRemoteConfigFlow(ConfigFlow, domain=DOMAIN):
return AndroidTVRemoteOptionsFlowHandler(config_entry)
class AndroidTVRemoteOptionsFlowHandler(OptionsFlow):
class AndroidTVRemoteOptionsFlowHandler(OptionsFlowWithReload):
"""Android TV Remote options flow."""
def __init__(self, config_entry: AndroidTVRemoteConfigEntry) -> None:

View File

@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
"iot_class": "local_polling",
"loggers": ["arcam"],
"requirements": ["arcam-fmj==1.8.1"],
"requirements": ["arcam-fmj==1.8.2"],
"ssdp": [
{
"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",
"iot_class": "cloud_push",
"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__)
VALID_REPAIR_TRANSLATION_KEYS = {
"connection_error",
"no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration",
"subscription_expired",
"warn_bad_custom_domain_configuration",
}

View File

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

View File

@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.106.0"],
"requirements": ["hass-nabucasa==0.107.1"],
"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": {
"title": "No subscription detected",
"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 homeassistant.components.sensor import SensorDeviceClass
from homeassistant.components.sensor.helpers import async_parse_date_datetime
from homeassistant.const import (
CONF_COMMAND,
CONF_NAME,
@ -188,16 +186,7 @@ class CommandSensor(ManualTriggerSensorEntity):
self.entity_id, variables, None
)
if self.device_class not in {
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._set_native_value_with_possible_timestamp(value)
self._process_manual_data(variables)
self.async_write_ha_state()

View File

@ -54,16 +54,20 @@ class 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."""
# Ruff doesn't understand this loop - the exception is always raised after the retries
exc = None
for i in range(API_RETRY_TIMES):
try:
return await func(*func_args)
except client_exceptions.ClientError as exception:
_LOGGER.error("Error connecting to Control4 account API: %s", exception)
if i == API_RETRY_TIMES - 1:
raise ConfigEntryNotReady(exception) from exception
_LOGGER.error(
"Try: %d, Error connecting to Control4 account API: %s",
i + 1,
exception,
)
exc = exception
raise ConfigEntryNotReady(exc) from exc
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,
)
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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:
"""Unload a config entry."""
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
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_PASSWORD,
@ -153,7 +157,7 @@ class Control4ConfigFlow(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler()
class OptionsFlowHandler(OptionsFlow):
class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option flow for Control4."""
async def async_step_init(

View File

@ -53,8 +53,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DenonavrConfigEntry) ->
raise ConfigEntryNotReady from ex
receiver = connect_denonavr.receiver
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.runtime_data = receiver
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")
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
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.core import callback
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})
class OptionsFlowHandler(OptionsFlow):
class OptionsFlowHandler(OptionsFlowWithReload):
"""Options for the component."""
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.
return
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
def on_state_changed(event: Event[EventStateChangedData]) -> None:
@ -334,19 +339,27 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
schedule_max_sub_interval_exceeded(new_state)
old_state = event.data["old_state"]
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:
# On first state change from none, update availability
self.async_write_ha_state()
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:
"""Handle the sensor state changes."""
if not _is_decimal_state(old_value):
if self._last_valid_state_time:
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:
# Sensor becomes valid for the first time, just keep the restored value
self.async_write_ha_state()
@ -358,12 +371,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
"" if unit is None else unit
)
self._prune_state_list(new_state.last_reported)
self._prune_state_list(new_timestamp)
try:
elapsed_time = (
new_state.last_reported - old_last_reported
).total_seconds()
elapsed_time = (new_timestamp - old_timestamp).total_seconds()
delta_value = Decimal(new_state.state) - Decimal(old_value)
new_derivative = (
delta_value
@ -392,12 +403,10 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
return
# add latest derivative to the window list
self._state_list.append(
(old_last_reported, new_state.last_reported, new_derivative)
)
self._state_list.append((old_timestamp, new_timestamp, new_derivative))
self._last_valid_state_time = (
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),
@ -405,9 +414,7 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if elapsed_time > self._time_window:
derivative = new_derivative
else:
derivative = self._calc_derivative_from_state_list(
new_state.last_reported
)
derivative = self._calc_derivative_from_state_list(new_timestamp)
self._write_native_value(derivative)
source_state = self.hass.states.get(self._sensor_source_id)

View File

@ -9,7 +9,7 @@
"password": "[%key:common::config_flow::data::password%]"
},
"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."
}
},
@ -22,8 +22,8 @@
}
},
"zeroconf_confirm": {
"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",
"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",
"data": {
"password": "[%key:common::config_flow::data::password%]"
},

View File

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

View File

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

View File

@ -8,7 +8,7 @@
"documentation": "https://www.home-assistant.io/integrations/dlna_dmr",
"iot_class": "local_push",
"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": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",

View File

@ -7,7 +7,7 @@
"dependencies": ["ssdp"],
"documentation": "https://www.home-assistant.io/integrations/dlna_dms",
"iot_class": "local_polling",
"requirements": ["async-upnp-client==0.44.0"],
"requirements": ["async-upnp-client==0.45.0"],
"ssdp": [
{
"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."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
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:
"""Unload dnsip config entry."""

View File

@ -14,7 +14,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_NAME, CONF_PORT
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."""
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()
entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
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:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import callback
@ -221,7 +221,7 @@ class EmoncmsConfigFlow(ConfigFlow, domain=DOMAIN):
)
class EmoncmsOptionsFlow(OptionsFlow):
class EmoncmsOptionsFlow(OptionsFlowWithReload):
"""Emoncms Options flow handler."""
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.SELECT)
ent_reg = er.async_get(hass)
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)
needed_platforms.update(INFO_TYPE_TO_PLATFORM[type(info)] for info in infos)
await self._ensure_platforms_loaded(hass, entry, needed_platforms)
# Make a dict of the EntityInfo by type and send

View File

@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==35.0.0",
"aioesphomeapi==37.0.2",
"esphome-dashboard-api==1.3.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.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.
# Cameras are accessed via local RTSP stream with unique credentials 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(
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
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_CUSTOMIZE,
CONF_IP_ADDRESS,
@ -386,7 +390,7 @@ class EzvizConfigFlow(ConfigFlow, domain=DOMAIN):
)
class EzvizOptionsFlowHandler(OptionsFlow):
class EzvizOptionsFlowHandler(OptionsFlowWithReload):
"""Handle EZVIZ client options."""
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()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
@ -46,10 +44,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: FeedReaderConfigEntry)
if len(entries) == 1:
hass.data.pop(MY_KEY)
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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_URL
from homeassistant.core import HomeAssistant, callback
@ -44,7 +44,7 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
) -> OptionsFlow:
) -> FeedReaderOptionsFlowHandler:
"""Get the options flow for this handler."""
return FeedReaderOptionsFlowHandler()
@ -119,11 +119,10 @@ class FeedReaderConfigFlow(ConfigFlow, domain=DOMAIN):
errors={"base": "url_error"},
)
self.hass.config_entries.async_update_entry(reconfigure_entry, data=user_input)
return self.async_abort(reason="reconfigure_successful")
return self.async_update_reload_and_abort(reconfigure_entry, data=user_input)
class FeedReaderOptionsFlowHandler(OptionsFlow):
class FeedReaderOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow."""
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(
entry, [Platform(entry.data[CONF_PLATFORM])]
)
entry.async_on_unload(entry.add_update_listener(update_listener))
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:
"""Migrate config entry."""
if config_entry.version > 2:

View File

@ -11,7 +11,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import (
CONF_FILE_PATH,
@ -131,7 +131,7 @@ class FileConfigFlowHandler(ConfigFlow, domain=DOMAIN):
return await self._async_handle_step(Platform.SENSOR.value, user_input)
class FileOptionsFlowHandler(OptionsFlow):
class FileOptionsFlowHandler(OptionsFlowWithReload):
"""Handle File options."""
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)
entry.async_on_unload(entry.add_update_listener(async_update_options))
return True
@ -57,10 +55,3 @@ async def async_unload_entry(
) -> bool:
"""Unload a config entry."""
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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
from homeassistant.core import callback
@ -88,7 +88,7 @@ class ForecastSolarFlowHandler(ConfigFlow, domain=DOMAIN):
)
class ForecastSolarOptionFlowHandler(OptionsFlow):
class ForecastSolarOptionFlowHandler(OptionsFlowWithReload):
"""Handle options."""
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:
hass.data[FRITZ_DATA_KEY] = FritzData()
entry.async_on_unload(entry.add_update_listener(update_listener))
# Load the other platforms like switch
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)
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,
DEFAULT_CONSIDER_HOME,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
OptionsFlowWithReload,
)
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@ -409,7 +413,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
)
class FritzBoxToolsOptionsFlowHandler(OptionsFlow):
class FritzBoxToolsOptionsFlowHandler(OptionsFlowWithReload):
"""Handle an options flow."""
async def async_step_init(

View File

@ -48,7 +48,6 @@ async def async_setup_entry(
raise ConfigEntryNotReady from ex
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)
return True
@ -59,10 +58,3 @@ async def async_unload_entry(
) -> bool:
"""Unloading the fritzbox_callmonitor 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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
from homeassistant.core import callback
@ -263,7 +263,7 @@ class FritzBoxCallMonitorConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful")
class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlow):
class FritzBoxCallMonitorOptionsFlowHandler(OptionsFlowWithReload):
"""Handle a fritzbox_callmonitor options flow."""
@classmethod

View File

@ -20,5 +20,5 @@
"documentation": "https://www.home-assistant.io/integrations/frontend",
"integration_type": "system",
"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 (
NumberDeviceClass,
NumberEntity,
NumberEntityDescription,
NumberMode,
@ -54,6 +55,7 @@ DESCRIPTIONS = (
native_step=60,
entity_category=EntityCategory.CONFIG,
char=Valve.manual_watering_time,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=Valve.remaining_open_time.uuid,
@ -64,6 +66,7 @@ DESCRIPTIONS = (
native_step=60.0,
entity_category=EntityCategory.DIAGNOSTIC,
char=Valve.remaining_open_time,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.rain_pause.uuid,
@ -75,6 +78,7 @@ DESCRIPTIONS = (
native_step=6 * 60.0,
entity_category=EntityCategory.CONFIG,
char=DeviceConfiguration.rain_pause,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=DeviceConfiguration.seasonal_adjust.uuid,
@ -86,6 +90,7 @@ DESCRIPTIONS = (
native_step=1.0,
entity_category=EntityCategory.CONFIG,
char=DeviceConfiguration.seasonal_adjust,
device_class=NumberDeviceClass.DURATION,
),
GardenaBluetoothNumberEntityDescription(
key=Sensor.threshold.uuid,
@ -153,6 +158,7 @@ class GardenaBluetoothRemainingOpenSetNumber(GardenaBluetoothEntity, NumberEntit
_attr_native_min_value = 0.0
_attr_native_max_value = 24 * 60
_attr_native_step = 1.0
_attr_device_class = NumberDeviceClass.DURATION
def __init__(
self,

View File

@ -6,7 +6,11 @@ from typing import Any
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.helpers.entity_platform import AddConfigEntryEntitiesCallback
@ -37,6 +41,7 @@ class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity):
_attr_is_closed: bool | None = None
_attr_reports_position = False
_attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE
_attr_device_class = ValveDeviceClass.WATER
characteristics = {
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)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True
@ -87,8 +86,3 @@ async def async_unload_entry(hass: HomeAssistant, entry: GithubConfigEntry) -> b
coordinator.unsubscribe()
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,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import HomeAssistant, callback
@ -214,7 +214,7 @@ class GitHubConfigFlow(ConfigFlow, domain=DOMAIN):
return OptionsFlowHandler()
class OptionsFlowHandler(OptionsFlow):
class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle a option flow for GitHub."""
async def async_step_init(

View File

@ -5,7 +5,7 @@ from __future__ import annotations
from dataclasses import asdict, fields
import datetime
from math import floor
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Literal
from dateutil.rrule import (
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()
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}

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)
entry.async_on_unload(entry.add_update_listener(update_listener))
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:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

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

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/homematicip_cloud",
"iot_class": "cloud_push",
"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)
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
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(
hass: HomeAssistant, config_entry: HoneywellConfigEntry
) -> bool:

View File

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

View File

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

View File

@ -6,6 +6,7 @@ import asyncio
from collections.abc import Callable
from datetime import timedelta
import logging
from typing import override
from aioautomower.exceptions import (
ApiError,
@ -60,7 +61,12 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
self._devices_last_update: set[str] = set()
self._zones_last_update: dict[str, set[str]] = {}
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:
"""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."""
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):
"""Replies available when the mower is connected."""

View File

@ -8,5 +8,5 @@
"iot_class": "cloud_push",
"loggers": ["aioautomower"],
"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 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):
"""Defining the Work area sensors with WorkAreaSensorEntityDescription."""

View File

@ -2,46 +2,28 @@
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.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, PLATFORMS
_LOGGER = logging.getLogger(__name__)
from .const import PLATFORMS
from .coordinator import HuumConfigEntry, HuumDataUpdateCoordinator
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."""
username = entry.data[CONF_USERNAME]
password = entry.data[CONF_PASSWORD]
coordinator = HuumDataUpdateCoordinator(
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 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)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

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

View File

@ -37,12 +37,12 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
errors = {}
if user_input is not None:
try:
huum_handler = Huum(
huum = Huum(
user_input[CONF_USERNAME],
user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass),
)
await huum_handler.status()
await huum.status()
except (Forbidden, NotAuthenticated):
# 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")

View File

@ -4,4 +4,4 @@ from homeassistant.const import Platform
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",
"name": "Huum",
"codeowners": ["@frwickst"],
"codeowners": ["@frwickst", "@vincentwolsink"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/huum",
"iot_class": "cloud_polling",

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"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_NAME,
CONF_UNIT_OF_MEASUREMENT,
MAX_LENGTH_STATE_STATE,
SERVICE_RELOAD,
)
from homeassistant.core import HomeAssistant, ServiceCall, callback
@ -51,8 +52,12 @@ STORAGE_VERSION = 1
STORAGE_FIELDS: VolDictType = {
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_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.All(
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_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
@ -84,8 +89,12 @@ CONFIG_SCHEMA = vol.Schema(
lambda value: value or {},
{
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int),
vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int),
vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.All(
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_ICON): cv.icon,
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,

View File

@ -463,7 +463,7 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state update when sub interval is configured."""
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
@ -472,13 +472,17 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state report when sub interval is configured."""
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
def _integrate_on_state_update_with_max_sub_interval(
self,
old_last_reported: datetime | None,
old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None,
new_state: State | None,
) -> None:
@ -489,7 +493,9 @@ class IntegrationSensor(RestoreSensor):
"""
self._cancel_max_sub_interval_exceeded_callback()
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_time = datetime.now(tz=UTC)
finally:
@ -503,7 +509,7 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor 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
@ -512,12 +518,16 @@ class IntegrationSensor(RestoreSensor):
) -> None:
"""Handle sensor state report."""
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(
self,
old_last_reported: datetime | None,
old_timestamp: datetime | None,
new_timestamp: datetime | None,
old_state: State | None,
new_state: State | None,
) -> None:
@ -531,16 +541,17 @@ class IntegrationSensor(RestoreSensor):
if old_state:
# state has changed, we recover old_state from the event
new_timestamp = new_state.last_updated
old_state_state = old_state.state
old_last_reported = old_state.last_reported
old_timestamp = old_state.last_reported
else:
# event state reported without any state change
# first state or event state reported without any state change
old_state_state = new_state.state
self._attr_available = True
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()
return
@ -551,11 +562,12 @@ class IntegrationSensor(RestoreSensor):
return
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(
(new_state.last_reported - old_last_reported).total_seconds()
(new_timestamp - old_timestamp).total_seconds()
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)

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")
isy.websocket.start()
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
entry.async_on_unload(
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
async def _async_update_listener(hass: HomeAssistant, entry: IsyConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
@callback
def _async_get_or_create_isy_device_in_registry(
hass: HomeAssistant, entry: IsyConfigEntry, isy: ISY

View File

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

View File

@ -9,7 +9,11 @@ import zoneinfo
from hdate.translator import Language
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_ELEVATION,
CONF_LANGUAGE,
@ -124,7 +128,7 @@ class JewishCalendarConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_update_reload_and_abort(reconfigure_entry, data=user_input)
class JewishCalendarOptionsFlowHandler(OptionsFlow):
class JewishCalendarOptionsFlowHandler(OptionsFlowWithReload):
"""Handle Jewish Calendar options."""
async def async_step_init(

View File

@ -33,8 +33,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: KeeneticConfigEntry) ->
router = KeeneticRouter(hass, entry)
await router.async_setup()
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.runtime_data = router
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -87,11 +85,6 @@ async def async_unload_entry(
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):
"""Populate default options."""
host: str = entry.data[CONF_HOST]

View File

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

View File

@ -101,19 +101,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Start a reauth flow
entry.async_start_reauth(hass)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
# Notify backup listeners
hass.async_create_task(_notify_backup_listeners(hass), eager_start=False)
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:
"""Unload config entry."""
# Notify backup listeners

View File

@ -13,7 +13,7 @@ from homeassistant.config_entries import (
ConfigFlow,
ConfigFlowResult,
ConfigSubentryFlow,
OptionsFlow,
OptionsFlowWithReload,
SubentryFlowResult,
)
from homeassistant.core import callback
@ -65,7 +65,7 @@ class KitchenSinkConfigFlow(ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful")
class OptionsFlowHandler(OptionsFlow):
class OptionsFlowHandler(OptionsFlowWithReload):
"""Handle options."""
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
entry.async_on_unload(entry.add_update_listener(async_update_entry))
if CONF_KNX_EXPOSE in config:
for expose_config in config[CONF_KNX_EXPOSE]:
knx_module.exposures.append(
@ -174,11 +172,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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:
"""Remove a config entry."""

View File

@ -23,7 +23,7 @@ from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import callback
@ -899,7 +899,7 @@ class KNXConfigFlow(ConfigFlow, domain=DOMAIN):
)
class KNXOptionsFlow(OptionsFlow):
class KNXOptionsFlow(OptionsFlowWithReload):
"""Handle KNX options."""
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)
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

View File

@ -21,7 +21,7 @@ from homeassistant.config_entries import (
SOURCE_RECONFIGURE,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
OptionsFlowWithReload,
)
from homeassistant.const import (
CONF_ADDRESS,
@ -363,7 +363,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN):
return LmOptionsFlowHandler()
class LmOptionsFlowHandler(OptionsFlow):
class LmOptionsFlowHandler(OptionsFlowWithReload):
"""Handles options flow for the component."""
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
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(update_listener))
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:
"""Unload lastfm config entry."""
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
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.core import callback
from homeassistant.helpers.selector import (
@ -155,7 +159,7 @@ class LastFmConfigFlowHandler(ConfigFlow, domain=DOMAIN):
)
class LastFmOptionsFlowHandler(OptionsFlow):
class LastFmOptionsFlowHandler(OptionsFlowWithReload):
"""LastFm Options flow handler."""
config_entry: LastFMConfigEntry

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import logging
from letpot.deviceclient import LetPotDeviceClient
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.core import HomeAssistant
@ -34,8 +34,8 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
self,
hass: HomeAssistant,
config_entry: LetPotConfigEntry,
info: AuthenticationInfo,
device: LetPotDevice,
device_client: LetPotDeviceClient,
) -> None:
"""Initialize coordinator."""
super().__init__(
@ -45,9 +45,8 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
name=f"LetPot {device.serial_number}",
update_interval=timedelta(minutes=10),
)
self._info = info
self.device = device
self.device_client = LetPotDeviceClient(info, device.serial_number)
self.device_client = device_client
def _handle_status_update(self, status: LetPotDeviceStatus) -> None:
"""Distribute status update to entities."""
@ -56,7 +55,9 @@ class LetPotDeviceCoordinator(DataUpdateCoordinator[LetPotDeviceStatus]):
async def _async_setup(self) -> None:
"""Set up subscription for coordinator."""
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:
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."""
try:
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:
raise UpdateFailed(exc) from exc

View File

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

View File

@ -6,6 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/letpot",
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["letpot"],
"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,
supported_fn=(
lambda coordinator: DeviceFeature.TEMPERATURE
in coordinator.device_client.device_features
in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
),
),
LetPotSensorEntityDescription(
@ -61,7 +63,9 @@ SENSORS: tuple[LetPotSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
supported_fn=(
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."""
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, ...] = (
@ -33,7 +33,9 @@ SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
key="alarm_sound",
translation_key="alarm_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,
supported_fn=lambda coordinator: coordinator.data.system_sound is not None,
),
@ -41,25 +43,35 @@ SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
key="auto_mode",
translation_key="auto_mode",
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,
supported_fn=(
lambda coordinator: DeviceFeature.PUMP_AUTO
in coordinator.device_client.device_features
in coordinator.device_client.device_info(
coordinator.device.serial_number
).features
),
),
LetPotSwitchEntityDescription(
key="power",
translation_key="power",
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,
),
LetPotSwitchEntityDescription(
key="pump_cycling",
translation_key="pump_cycling",
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,
),
)
@ -104,11 +116,13 @@ class LetPotSwitchEntity(LetPotEntity, SwitchEntity):
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""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
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off."""
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