mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Merge branch 'dev' into state_selector_climate_hvac_mode
This commit is contained in:
commit
1d2dccf259
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@ -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+
|
||||
|
3
.github/dependabot.yml
vendored
3
.github/dependabot.yml
vendored
@ -6,3 +6,6 @@ updates:
|
||||
interval: daily
|
||||
time: "06:00"
|
||||
open-pull-requests-limit: 10
|
||||
labels:
|
||||
- dependency
|
||||
- github_actions
|
||||
|
@ -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: |
|
||||
|
@ -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
4
CODEOWNERS
generated
@ -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
|
||||
|
@ -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."]
|
||||
}
|
||||
|
@ -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: |
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioamazondevices==3.2.10"]
|
||||
"requirements": ["aioamazondevices==3.5.0"]
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
1
homeassistant/components/bauknecht/__init__.py
Normal file
1
homeassistant/components/bauknecht/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Bauknecht virtual integration."""
|
6
homeassistant/components/bauknecht/manifest.json
Normal file
6
homeassistant/components/bauknecht/manifest.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"domain": "bauknecht",
|
||||
"name": "Bauknecht",
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "whirlpool"
|
||||
}
|
@ -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"]
|
||||
}
|
||||
|
@ -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",
|
||||
}
|
||||
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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}."
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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%]"
|
||||
},
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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: |
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
],
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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:
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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(
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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,
|
||||
|
@ -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."""
|
||||
|
@ -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."""
|
||||
|
@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioautomower"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["aioautomower==1.2.2"]
|
||||
"requirements": ["aioautomower==2.0.0"]
|
||||
}
|
||||
|
@ -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."""
|
||||
|
@ -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)
|
||||
|
42
homeassistant/components/huum/binary_sensor.py
Normal file
42
homeassistant/components/huum/binary_sensor.py
Normal 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
|
@ -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
|
||||
|
@ -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")
|
||||
|
@ -4,4 +4,4 @@ from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "huum"
|
||||
|
||||
PLATFORMS = [Platform.CLIMATE]
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.CLIMATE]
|
||||
|
60
homeassistant/components/huum/coordinator.py
Normal file
60
homeassistant/components/huum/coordinator.py
Normal 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
|
24
homeassistant/components/huum/entity.py
Normal file
24
homeassistant/components/huum/entity.py
Normal 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",
|
||||
)
|
@ -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",
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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."""
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -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
Loading…
x
Reference in New Issue
Block a user