mirror of
https://github.com/home-assistant/core.git
synced 2025-10-01 07:49:26 +00:00
Compare commits
2 Commits
select-sel
...
uptimerobo
Author | SHA1 | Date | |
---|---|---|---|
![]() |
80285fd3e6 | ||
![]() |
ab4086be18 |
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -15,7 +14,7 @@ from airos.exceptions import (
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PASSWORD,
|
||||
@@ -25,11 +24,6 @@ from homeassistant.const import (
|
||||
)
|
||||
from homeassistant.data_entry_flow import section
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
TextSelector,
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, SECTION_ADVANCED_SETTINGS
|
||||
from .coordinator import AirOS8
|
||||
@@ -60,107 +54,50 @@ class AirOSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
super().__init__()
|
||||
self.airos_device: AirOS8
|
||||
self.errors: dict[str, str] = {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
self,
|
||||
user_input: dict[str, Any] | None = None,
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the manual input of host and credentials."""
|
||||
self.errors = {}
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
validated_info = await self._validate_and_get_device_info(user_input)
|
||||
if validated_info:
|
||||
return self.async_create_entry(
|
||||
title=validated_info["title"],
|
||||
data=validated_info["data"],
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=self.errors
|
||||
)
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
async def _validate_and_get_device_info(
|
||||
self, config_data: dict[str, Any]
|
||||
) -> dict[str, Any] | None:
|
||||
"""Validate user input with the device API."""
|
||||
# By default airOS 8 comes with self-signed SSL certificates,
|
||||
# with no option in the web UI to change or upload a custom certificate.
|
||||
session = async_get_clientsession(
|
||||
self.hass,
|
||||
verify_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_VERIFY_SSL],
|
||||
)
|
||||
airos_device = AirOS8(
|
||||
host=user_input[CONF_HOST],
|
||||
username=user_input[CONF_USERNAME],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=session,
|
||||
use_ssl=user_input[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
)
|
||||
try:
|
||||
await airos_device.login()
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
airos_device = AirOS8(
|
||||
host=config_data[CONF_HOST],
|
||||
username=config_data[CONF_USERNAME],
|
||||
password=config_data[CONF_PASSWORD],
|
||||
session=session,
|
||||
use_ssl=config_data[SECTION_ADVANCED_SETTINGS][CONF_SSL],
|
||||
)
|
||||
try:
|
||||
await airos_device.login()
|
||||
airos_data = await airos_device.status()
|
||||
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
self.errors["base"] = "cannot_connect"
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
self.errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
self.errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception during credential validation")
|
||||
self.errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(airos_data.derived.mac)
|
||||
|
||||
if self.source == SOURCE_REAUTH:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
except (
|
||||
AirOSConnectionSetupError,
|
||||
AirOSDeviceConnectionError,
|
||||
):
|
||||
errors["base"] = "cannot_connect"
|
||||
except (AirOSConnectionAuthenticationError, AirOSDataMissingError):
|
||||
errors["base"] = "invalid_auth"
|
||||
except AirOSKeyDataMissingError:
|
||||
errors["base"] = "key_data_missing"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(airos_data.derived.mac)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return {"title": airos_data.host.hostname, "data": config_data}
|
||||
|
||||
return None
|
||||
|
||||
async def async_step_reauth(
|
||||
self,
|
||||
user_input: Mapping[str, Any],
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauthentication upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm(user_input)
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self,
|
||||
user_input: Mapping[str, Any],
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauthentication upon an API authentication error."""
|
||||
self.errors = {}
|
||||
|
||||
if user_input:
|
||||
validate_data = {**self._get_reauth_entry().data, **user_input}
|
||||
if await self._validate_and_get_device_info(config_data=validate_data):
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reauth_entry(),
|
||||
data_updates=validate_data,
|
||||
return self.async_create_entry(
|
||||
title=airos_data.host.hostname, data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): TextSelector(
|
||||
TextSelectorConfig(
|
||||
type=TextSelectorType.PASSWORD,
|
||||
autocomplete="current-password",
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
errors=self.errors,
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
@@ -14,7 +14,7 @@ from airos.exceptions import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
@@ -47,9 +47,9 @@ class AirOSDataUpdateCoordinator(DataUpdateCoordinator[AirOS8Data]):
|
||||
try:
|
||||
await self.airos_device.login()
|
||||
return await self.airos_device.status()
|
||||
except AirOSConnectionAuthenticationError as err:
|
||||
except (AirOSConnectionAuthenticationError,) as err:
|
||||
_LOGGER.exception("Error authenticating with airOS device")
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN, translation_key="invalid_auth"
|
||||
) from err
|
||||
except (
|
||||
|
@@ -2,14 +2,6 @@
|
||||
"config": {
|
||||
"flow_title": "Ubiquiti airOS device",
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::airos::config::step::user::data_description::password%]"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
@@ -42,9 +34,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unique_id_mismatch": "Re-authentication should be used for the same device not a new one"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
@@ -32,7 +32,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
@@ -47,9 +46,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.SENSOR,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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, (Platform.SENSOR,))
|
||||
|
@@ -140,7 +140,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 4
|
||||
|
@@ -69,9 +69,7 @@ class EcovacsMap(
|
||||
await super().async_added_to_hass()
|
||||
|
||||
async def on_info(event: CachedMapInfoEvent) -> None:
|
||||
for map_obj in event.maps:
|
||||
if map_obj.using:
|
||||
self._attr_extra_state_attributes["map_name"] = map_obj.name
|
||||
self._attr_extra_state_attributes["map_name"] = event.name
|
||||
|
||||
async def on_changed(event: MapChangedEvent) -> None:
|
||||
self._attr_image_last_updated = event.when
|
||||
|
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/ecovacs",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["sleekxmppfs", "sucks", "deebot_client"],
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==15.0.0"]
|
||||
"requirements": ["py-sucks==0.9.11", "deebot-client==14.0.0"]
|
||||
}
|
||||
|
@@ -3,15 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from enum import IntEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyephember2.pyephember2 import (
|
||||
EphEmber,
|
||||
ZoneMode,
|
||||
boiler_state,
|
||||
zone_current_temperature,
|
||||
zone_is_active,
|
||||
zone_is_hotwater,
|
||||
zone_mode,
|
||||
zone_name,
|
||||
@@ -54,15 +53,6 @@ EPH_TO_HA_STATE = {
|
||||
"OFF": HVACMode.OFF,
|
||||
}
|
||||
|
||||
|
||||
class EPHBoilerStates(IntEnum):
|
||||
"""Boiler states for a zone given by the api."""
|
||||
|
||||
FIXME = 0
|
||||
OFF = 1
|
||||
ON = 2
|
||||
|
||||
|
||||
HA_STATE_TO_EPH = {value: key for key, value in EPH_TO_HA_STATE.items()}
|
||||
|
||||
|
||||
@@ -133,7 +123,7 @@ class EphEmberThermostat(ClimateEntity):
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction:
|
||||
"""Return current HVAC action."""
|
||||
if boiler_state(self._zone) == EPHBoilerStates.ON:
|
||||
if zone_is_active(self._zone):
|
||||
return HVACAction.HEATING
|
||||
|
||||
return HVACAction.IDLE
|
||||
|
@@ -10,6 +10,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Filter 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
|
||||
|
||||
@@ -17,3 +18,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Filter 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)
|
||||
|
@@ -246,7 +246,6 @@ class FilterConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -108,7 +108,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_HUMIDIFIER: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidifer,
|
||||
@@ -141,7 +140,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
@@ -150,6 +148,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, (Platform.HUMIDIFIER,))
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -187,6 +186,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -96,7 +96,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -35,7 +35,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_HEATER: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the heater, but
|
||||
@@ -68,7 +67,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SENSOR: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_track_entity_registry_updated_event(
|
||||
@@ -77,6 +75,7 @@ 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(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -114,6 +113,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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)
|
||||
|
@@ -104,7 +104,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -141,9 +141,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["group_type"],)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -329,7 +329,6 @@ class GroupConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
@callback
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
|
@@ -65,7 +65,6 @@ async def async_setup_entry(
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
@@ -87,6 +86,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -130,3 +130,8 @@ async def async_unload_entry(
|
||||
) -> bool:
|
||||
"""Unload History stats 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)
|
||||
|
@@ -162,7 +162,6 @@ class HistoryStatsConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -22,6 +22,7 @@ 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
|
||||
|
||||
@@ -29,3 +30,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Local file 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)
|
||||
|
@@ -65,7 +65,6 @@ class LocalFileConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -39,7 +39,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_INDOOR_HUMIDITY: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
# We use async_handle_source_entity_changes to track changes to the humidity
|
||||
@@ -80,7 +79,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, temp_sensor: data["entity_id"]},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
return async_sensor_updated
|
||||
|
||||
@@ -91,6 +89,7 @@ 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
|
||||
|
||||
@@ -100,6 +99,11 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
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)
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
|
||||
|
@@ -100,7 +100,6 @@ class MoldIndicatorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 2
|
||||
|
@@ -51,10 +51,7 @@ from homeassistant.components.sensor import (
|
||||
DEVICE_CLASS_UNITS,
|
||||
STATE_CLASS_UNITS,
|
||||
SensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.sensor.helpers import (
|
||||
create_sensor_device_class_select_selector,
|
||||
create_sensor_state_class_select_selector,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.components.switch import SwitchDeviceClass
|
||||
from homeassistant.config_entries import (
|
||||
@@ -706,6 +703,14 @@ SCALE_SELECTOR = NumberSelector(
|
||||
step=1,
|
||||
)
|
||||
)
|
||||
SENSOR_DEVICE_CLASS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorDeviceClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="device_class_sensor",
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[EntityCategory.DIAGNOSTIC.value],
|
||||
@@ -714,6 +719,13 @@ SENSOR_ENTITY_CATEGORY_SELECTOR = SelectSelector(
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
SENSOR_STATE_CLASS_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorStateClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key=CONF_STATE_CLASS,
|
||||
)
|
||||
)
|
||||
SUPPORTED_COLOR_MODES_SELECTOR = SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[platform.value for platform in VALID_COLOR_MODES],
|
||||
@@ -1272,12 +1284,10 @@ PLATFORM_ENTITY_FIELDS: dict[str, dict[str, PlatformField]] = {
|
||||
Platform.NOTIFY.value: {},
|
||||
Platform.SENSOR.value: {
|
||||
CONF_DEVICE_CLASS: PlatformField(
|
||||
selector=create_sensor_device_class_select_selector(),
|
||||
required=False,
|
||||
selector=SENSOR_DEVICE_CLASS_SELECTOR, required=False
|
||||
),
|
||||
CONF_STATE_CLASS: PlatformField(
|
||||
selector=create_sensor_state_class_select_selector(),
|
||||
required=False,
|
||||
selector=SENSOR_STATE_CLASS_SELECTOR, required=False
|
||||
),
|
||||
CONF_UNIT_OF_MEASUREMENT: PlatformField(
|
||||
selector=unit_of_measurement_selector,
|
||||
|
@@ -1200,6 +1200,67 @@
|
||||
"window": "[%key:component::cover::entity_component::window::name%]"
|
||||
}
|
||||
},
|
||||
"device_class_sensor": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||
"current": "[%key:component::sensor::entity_component::current::name%]",
|
||||
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
||||
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||
"enum": "Enumeration",
|
||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
||||
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
||||
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
||||
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
||||
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
||||
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds_parts::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||
}
|
||||
},
|
||||
"device_class_switch": {
|
||||
"options": {
|
||||
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
|
||||
@@ -1261,6 +1322,14 @@
|
||||
"custom": "Custom"
|
||||
}
|
||||
},
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
},
|
||||
"supported_color_modes": {
|
||||
"options": {
|
||||
"onoff": "[%key:component::light::entity_component::_::state_attributes::color_mode::state::onoff%]",
|
||||
|
@@ -52,10 +52,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: OnkyoConfigEntry) -> boo
|
||||
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError as exc:
|
||||
raise ConfigEntryNotReady(f"Timed out interviewing: {host}") from exc
|
||||
except OSError as exc:
|
||||
raise ConfigEntryNotReady(f"Unexpected exception interviewing: {host}") from exc
|
||||
raise ConfigEntryNotReady(f"Unable to connect to: {host}") from exc
|
||||
if info is None:
|
||||
raise ConfigEntryNotReady(f"Unable to connect to: {host}")
|
||||
|
||||
manager = ReceiverManager(hass, entry, info)
|
||||
|
||||
|
@@ -109,22 +109,24 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.debug("Config flow manual: %s", host)
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
||||
errors["base"] = "cannot_connect"
|
||||
except OSError:
|
||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
self._receiver_info = info
|
||||
|
||||
await self.async_set_unique_id(info.identifier, raise_on_progress=False)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
if info is None:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
self._abort_if_unique_id_configured()
|
||||
self._receiver_info = info
|
||||
|
||||
return await self.async_step_configure_receiver()
|
||||
await self.async_set_unique_id(
|
||||
info.identifier, raise_on_progress=False
|
||||
)
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch()
|
||||
else:
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
return await self.async_step_configure_receiver()
|
||||
|
||||
suggested_values = user_input
|
||||
if suggested_values is None and self.source == SOURCE_RECONFIGURE:
|
||||
@@ -212,13 +214,14 @@ class OnkyoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
info = await async_interview(host)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning("Timed out interviewing: %s", host)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except OSError:
|
||||
_LOGGER.exception("Unexpected exception interviewing: %s", host)
|
||||
_LOGGER.exception("Unexpected exception interviewing host %s", host)
|
||||
return self.async_abort(reason="unknown")
|
||||
|
||||
if info is None:
|
||||
_LOGGER.debug("SSDP eiscp is None: %s", host)
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
|
||||
await self.async_set_unique_id(info.identifier)
|
||||
self._abort_if_unique_id_configured(updates={CONF_HOST: info.host})
|
||||
|
||||
|
@@ -124,10 +124,13 @@ class ReceiverManager:
|
||||
self.callbacks.clear()
|
||||
|
||||
|
||||
async def async_interview(host: str) -> ReceiverInfo:
|
||||
async def async_interview(host: str) -> ReceiverInfo | None:
|
||||
"""Interview the receiver."""
|
||||
async with asyncio.timeout(DEVICE_INTERVIEW_TIMEOUT):
|
||||
return await aioonkyo.interview(host)
|
||||
info: ReceiverInfo | None = None
|
||||
with contextlib.suppress(asyncio.TimeoutError):
|
||||
async with asyncio.timeout(DEVICE_INTERVIEW_TIMEOUT):
|
||||
info = await aioonkyo.interview(host)
|
||||
return info
|
||||
|
||||
|
||||
async def async_discover(hass: HomeAssistant) -> Iterable[ReceiverInfo]:
|
||||
|
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -88,48 +87,6 @@ class PortainerConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Perform reauth when Portainer API authentication fails."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth: ask for new API token and validate."""
|
||||
errors: dict[str, str] = {}
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
if user_input is not None:
|
||||
try:
|
||||
await _validate_input(
|
||||
self.hass,
|
||||
data={
|
||||
**reauth_entry.data,
|
||||
CONF_API_TOKEN: user_input[CONF_API_TOKEN],
|
||||
},
|
||||
)
|
||||
except CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except PortainerTimeout:
|
||||
errors["base"] = "timeout_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
class CannotConnect(HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
@@ -18,7 +18,7 @@ from pyportainer.models.portainer import Endpoint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_URL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -66,7 +66,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
try:
|
||||
await self.portainer.get_endpoints()
|
||||
except PortainerAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise ConfigEntryError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
@@ -94,7 +94,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
endpoints = await self.portainer.get_endpoints()
|
||||
except PortainerAuthenticationError as err:
|
||||
_LOGGER.error("Authentication error: %s", repr(err))
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
@@ -121,7 +121,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
|
||||
) from err
|
||||
except PortainerAuthenticationError as err:
|
||||
_LOGGER.exception("Authentication error")
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_auth",
|
||||
translation_placeholders={"error": repr(err)},
|
||||
|
@@ -13,15 +13,6 @@
|
||||
"verify_ssl": "Whether to verify SSL certificates. Disable only if you have a self-signed certificate"
|
||||
},
|
||||
"description": "You can create an access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The new API access token for authenticating with Portainer"
|
||||
},
|
||||
"description": "The access token for your Portainer instance needs to be re-authenticated. You can create a new access token in the Portainer UI. Go to **My account > Access tokens** and select **Add access token**"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -31,8 +22,7 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"device": {
|
||||
|
@@ -9,9 +9,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["entity_type"],)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -184,7 +184,6 @@ class RandomConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
@callback
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
|
@@ -6,14 +6,9 @@ from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import DOMAIN, SensorDeviceClass, SensorStateClass
|
||||
from . import SensorDeviceClass
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -42,31 +37,3 @@ def async_parse_date_datetime(
|
||||
|
||||
_LOGGER.warning("%s rendered invalid date %s", entity_id, value)
|
||||
return None
|
||||
|
||||
|
||||
@callback
|
||||
def create_sensor_device_class_select_selector() -> SelectSelector:
|
||||
"""Create sensor device class select selector."""
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorDeviceClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="device_class",
|
||||
translation_domain=DOMAIN,
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def create_sensor_state_class_select_selector() -> SelectSelector:
|
||||
"""Create sensor state class select selector."""
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SensorStateClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="state_class",
|
||||
translation_domain=DOMAIN,
|
||||
sort=True,
|
||||
)
|
||||
)
|
||||
|
@@ -334,76 +334,5 @@
|
||||
"title": "The unit of {statistic_id} has changed",
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_class": {
|
||||
"options": {
|
||||
"absolute_humidity": "[%key:component::sensor::entity_component::absolute_humidity::name%]",
|
||||
"apparent_power": "[%key:component::sensor::entity_component::apparent_power::name%]",
|
||||
"area": "[%key:component::sensor::entity_component::area::name%]",
|
||||
"aqi": "[%key:component::sensor::entity_component::aqi::name%]",
|
||||
"atmospheric_pressure": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]",
|
||||
"battery": "[%key:component::sensor::entity_component::battery::name%]",
|
||||
"blood_glucose_concentration": "[%key:component::sensor::entity_component::blood_glucose_concentration::name%]",
|
||||
"carbon_dioxide": "[%key:component::sensor::entity_component::carbon_dioxide::name%]",
|
||||
"carbon_monoxide": "[%key:component::sensor::entity_component::carbon_monoxide::name%]",
|
||||
"conductivity": "[%key:component::sensor::entity_component::conductivity::name%]",
|
||||
"current": "[%key:component::sensor::entity_component::current::name%]",
|
||||
"data_rate": "[%key:component::sensor::entity_component::data_rate::name%]",
|
||||
"data_size": "[%key:component::sensor::entity_component::data_size::name%]",
|
||||
"date": "[%key:component::sensor::entity_component::date::name%]",
|
||||
"distance": "[%key:component::sensor::entity_component::distance::name%]",
|
||||
"duration": "[%key:component::sensor::entity_component::duration::name%]",
|
||||
"energy": "[%key:component::sensor::entity_component::energy::name%]",
|
||||
"energy_distance": "[%key:component::sensor::entity_component::energy_distance::name%]",
|
||||
"energy_storage": "[%key:component::sensor::entity_component::energy_storage::name%]",
|
||||
"enum": "Enumeration",
|
||||
"frequency": "[%key:component::sensor::entity_component::frequency::name%]",
|
||||
"gas": "[%key:component::sensor::entity_component::gas::name%]",
|
||||
"humidity": "[%key:component::sensor::entity_component::humidity::name%]",
|
||||
"illuminance": "[%key:component::sensor::entity_component::illuminance::name%]",
|
||||
"irradiance": "[%key:component::sensor::entity_component::irradiance::name%]",
|
||||
"moisture": "[%key:component::sensor::entity_component::moisture::name%]",
|
||||
"monetary": "[%key:component::sensor::entity_component::monetary::name%]",
|
||||
"nitrogen_dioxide": "[%key:component::sensor::entity_component::nitrogen_dioxide::name%]",
|
||||
"nitrogen_monoxide": "[%key:component::sensor::entity_component::nitrogen_monoxide::name%]",
|
||||
"nitrous_oxide": "[%key:component::sensor::entity_component::nitrous_oxide::name%]",
|
||||
"ozone": "[%key:component::sensor::entity_component::ozone::name%]",
|
||||
"ph": "[%key:component::sensor::entity_component::ph::name%]",
|
||||
"pm1": "[%key:component::sensor::entity_component::pm1::name%]",
|
||||
"pm10": "[%key:component::sensor::entity_component::pm10::name%]",
|
||||
"pm25": "[%key:component::sensor::entity_component::pm25::name%]",
|
||||
"power": "[%key:component::sensor::entity_component::power::name%]",
|
||||
"power_factor": "[%key:component::sensor::entity_component::power_factor::name%]",
|
||||
"precipitation": "[%key:component::sensor::entity_component::precipitation::name%]",
|
||||
"precipitation_intensity": "[%key:component::sensor::entity_component::precipitation_intensity::name%]",
|
||||
"pressure": "[%key:component::sensor::entity_component::pressure::name%]",
|
||||
"reactive_power": "[%key:component::sensor::entity_component::reactive_power::name%]",
|
||||
"signal_strength": "[%key:component::sensor::entity_component::signal_strength::name%]",
|
||||
"sound_pressure": "[%key:component::sensor::entity_component::sound_pressure::name%]",
|
||||
"speed": "[%key:component::sensor::entity_component::speed::name%]",
|
||||
"sulphur_dioxide": "[%key:component::sensor::entity_component::sulphur_dioxide::name%]",
|
||||
"temperature": "[%key:component::sensor::entity_component::temperature::name%]",
|
||||
"timestamp": "[%key:component::sensor::entity_component::timestamp::name%]",
|
||||
"volatile_organic_compounds": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"volatile_organic_compounds_parts": "[%key:component::sensor::entity_component::volatile_organic_compounds::name%]",
|
||||
"voltage": "[%key:component::sensor::entity_component::voltage::name%]",
|
||||
"volume": "[%key:component::sensor::entity_component::volume::name%]",
|
||||
"volume_flow_rate": "[%key:component::sensor::entity_component::volume_flow_rate::name%]",
|
||||
"volume_storage": "[%key:component::sensor::entity_component::volume_storage::name%]",
|
||||
"water": "[%key:component::sensor::entity_component::water::name%]",
|
||||
"weight": "[%key:component::sensor::entity_component::weight::name%]",
|
||||
"wind_direction": "[%key:component::sensor::entity_component::wind_direction::name%]",
|
||||
"wind_speed": "[%key:component::sensor::entity_component::wind_speed::name%]"
|
||||
}
|
||||
},
|
||||
"state_class": {
|
||||
"options": {
|
||||
"measurement": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement%]",
|
||||
"measurement_angle": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::measurement_angle%]",
|
||||
"total": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total%]",
|
||||
"total_increasing": "[%key:component::sensor::entity_component::_::state_attributes::state_class::state::total_increasing%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,25 +0,0 @@
|
||||
"""Helpers for switch entities."""
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectSelector,
|
||||
SelectSelectorConfig,
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from . import DOMAIN, SwitchDeviceClass
|
||||
|
||||
|
||||
@callback
|
||||
def create_switch_device_class_select_selector() -> SelectSelector:
|
||||
"""Create sensor device class select selector."""
|
||||
|
||||
return SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[device_class.value for device_class in SwitchDeviceClass],
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
translation_key="device_class",
|
||||
translation_domain=DOMAIN,
|
||||
sort=True,
|
||||
)
|
||||
)
|
@@ -55,13 +55,5 @@
|
||||
"name": "[%key:common::action::toggle%]",
|
||||
"description": "Toggles a switch on/off."
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"device_class": {
|
||||
"options": {
|
||||
"outlet": "[%key:component::switch::entity_component::outlet::name%]",
|
||||
"switch": "[%key:component::switch::title%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -52,7 +52,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
@@ -70,6 +69,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
source_entity_removed=source_entity_removed,
|
||||
)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options[CONF_TARGET_DOMAIN],)
|
||||
@@ -113,6 +113,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -56,7 +56,6 @@ class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
|
@@ -56,6 +56,7 @@ async def async_setup_entry(
|
||||
entry.runtime_data = SystemMonitorData(coordinator, psutil_wrapper)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
return True
|
||||
|
||||
|
||||
@@ -66,6 +67,11 @@ async def async_unload_entry(
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: SystemMonitorConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_migrate_entry(
|
||||
hass: HomeAssistant, entry: SystemMonitorConfigEntry
|
||||
) -> bool:
|
||||
|
@@ -92,8 +92,6 @@ class SystemMonitorConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 3
|
||||
|
||||
|
@@ -102,9 +102,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await hass.config_entries.async_forward_entry_setups(
|
||||
entry, (entry.options["template_type"],)
|
||||
)
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -695,7 +695,6 @@ class TemplateConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
@callback
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
|
@@ -13,9 +13,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, (Platform.BINARY_SENSOR,)
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
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(
|
||||
|
@@ -43,7 +43,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -139,11 +139,7 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
raise DeviceNotFound("Unable to find device")
|
||||
|
||||
try:
|
||||
client = await Client.connect(
|
||||
device,
|
||||
self._notify_callback,
|
||||
disconnected_callback=self._disconnected_callback,
|
||||
)
|
||||
client = await Client.connect(device, self._notify_callback)
|
||||
except BleakError as exc:
|
||||
self.logger.debug("Connection failed", exc_info=True)
|
||||
raise DeviceNotFound("Unable to connect to device") from exc
|
||||
@@ -173,6 +169,9 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
self.client = None
|
||||
|
||||
async def _get_connected_client(self) -> Client:
|
||||
if self.client and not self.client.is_connected:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
if self.client:
|
||||
return self.client
|
||||
|
||||
@@ -197,12 +196,6 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
|
||||
async def _async_update_data(self) -> dict[tuple[int, int | None], Packet]:
|
||||
"""Poll the device."""
|
||||
if self.client and not self.client.is_connected:
|
||||
await self.client.disconnect()
|
||||
self.client = None
|
||||
self._async_request_refresh_soon()
|
||||
raise DeviceFailed("Device was disconnected")
|
||||
|
||||
client = await self._get_connected_client()
|
||||
try:
|
||||
await client.request(PacketA0Notify)
|
||||
@@ -213,17 +206,6 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
raise DeviceFailed(f"Device failed {exc}") from exc
|
||||
return self.data
|
||||
|
||||
@callback
|
||||
def _async_request_refresh_soon(self) -> None:
|
||||
self.config_entry.async_create_task(
|
||||
self.hass, self.async_request_refresh(), eager_start=False
|
||||
)
|
||||
|
||||
@callback
|
||||
def _disconnected_callback(self) -> None:
|
||||
"""Handle Bluetooth device being disconnected."""
|
||||
self._async_request_refresh_soon()
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
@@ -231,5 +213,5 @@ class ToGrillCoordinator(DataUpdateCoordinator[dict[tuple[int, int | None], Pack
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Handle a Bluetooth event."""
|
||||
if isinstance(self.last_exception, DeviceNotFound):
|
||||
self._async_request_refresh_soon()
|
||||
if not self.client and isinstance(self.last_exception, DeviceNotFound):
|
||||
self.hass.async_create_task(self.async_refresh())
|
||||
|
@@ -36,7 +36,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_ENTITY_ID: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
async def source_entity_removed() -> None:
|
||||
# The source entity has been removed, we remove the config entry because
|
||||
@@ -58,6 +57,7 @@ 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(config_entry_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
@@ -96,6 +96,11 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an Trend 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)
|
||||
|
@@ -110,7 +110,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
options_flow = {
|
||||
"init": SchemaFlowFormStep(get_extended_options_schema),
|
||||
}
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyuptimerobot import UptimeRobotMonitor
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
@@ -12,6 +14,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import UptimeRobotConfigEntry
|
||||
from .entity import UptimeRobotEntity
|
||||
from .utils import new_device_listener
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
@@ -25,29 +28,23 @@ async def async_setup_entry(
|
||||
"""Set up the UptimeRobot binary_sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
entities: list[UptimeRobotBinarySensor] = []
|
||||
for monitor in coordinator.data:
|
||||
if monitor.id in known_devices:
|
||||
continue
|
||||
known_devices.add(monitor.id)
|
||||
entities.append(
|
||||
UptimeRobotBinarySensor(
|
||||
coordinator,
|
||||
BinarySensorEntityDescription(
|
||||
key=str(monitor.id),
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
def _add_new_entities(new_monitors: list[UptimeRobotMonitor]) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
UptimeRobotBinarySensor(
|
||||
coordinator,
|
||||
BinarySensorEntityDescription(
|
||||
key=str(monitor.id),
|
||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
for monitor in new_monitors
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
entry.async_on_unload(new_device_listener(coordinator, _add_new_entities))
|
||||
|
||||
|
||||
class UptimeRobotBinarySensor(UptimeRobotEntity, BinarySensorEntity):
|
||||
|
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pyuptimerobot import UptimeRobotMonitor
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
@@ -13,6 +15,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import UptimeRobotConfigEntry
|
||||
from .entity import UptimeRobotEntity
|
||||
from .utils import new_device_listener
|
||||
|
||||
SENSORS_INFO = {
|
||||
0: "pause",
|
||||
@@ -34,38 +37,32 @@ async def async_setup_entry(
|
||||
"""Set up the UptimeRobot sensors."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
entities: list[UptimeRobotSensor] = []
|
||||
for monitor in coordinator.data:
|
||||
if monitor.id in known_devices:
|
||||
continue
|
||||
known_devices.add(monitor.id)
|
||||
entities.append(
|
||||
UptimeRobotSensor(
|
||||
coordinator,
|
||||
SensorEntityDescription(
|
||||
key=str(monitor.id),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"down",
|
||||
"not_checked_yet",
|
||||
"pause",
|
||||
"seems_down",
|
||||
"up",
|
||||
],
|
||||
translation_key="monitor_status",
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
def _add_new_entities(new_monitors: list[UptimeRobotMonitor]) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
UptimeRobotSensor(
|
||||
coordinator,
|
||||
SensorEntityDescription(
|
||||
key=str(monitor.id),
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=[
|
||||
"down",
|
||||
"not_checked_yet",
|
||||
"pause",
|
||||
"seems_down",
|
||||
"up",
|
||||
],
|
||||
translation_key="monitor_status",
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
for monitor in new_monitors
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
entry.async_on_unload(new_device_listener(coordinator, _add_new_entities))
|
||||
|
||||
|
||||
class UptimeRobotSensor(UptimeRobotEntity, SensorEntity):
|
||||
|
@@ -4,7 +4,11 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyuptimerobot import UptimeRobotAuthenticationException, UptimeRobotException
|
||||
from pyuptimerobot import (
|
||||
UptimeRobotAuthenticationException,
|
||||
UptimeRobotException,
|
||||
UptimeRobotMonitor,
|
||||
)
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
SwitchDeviceClass,
|
||||
@@ -18,6 +22,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from .const import API_ATTR_OK, DOMAIN
|
||||
from .coordinator import UptimeRobotConfigEntry
|
||||
from .entity import UptimeRobotEntity
|
||||
from .utils import new_device_listener
|
||||
|
||||
# Limit the number of parallel updates to 1
|
||||
PARALLEL_UPDATES = 1
|
||||
@@ -31,29 +36,23 @@ async def async_setup_entry(
|
||||
"""Set up the UptimeRobot switches."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_device() -> None:
|
||||
entities: list[UptimeRobotSwitch] = []
|
||||
for monitor in coordinator.data:
|
||||
if monitor.id in known_devices:
|
||||
continue
|
||||
known_devices.add(monitor.id)
|
||||
entities.append(
|
||||
UptimeRobotSwitch(
|
||||
coordinator,
|
||||
SwitchEntityDescription(
|
||||
key=str(monitor.id),
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
def _add_new_entities(new_monitors: list[UptimeRobotMonitor]) -> None:
|
||||
"""Add entities for new monitors."""
|
||||
entities = [
|
||||
UptimeRobotSwitch(
|
||||
coordinator,
|
||||
SwitchEntityDescription(
|
||||
key=str(monitor.id),
|
||||
device_class=SwitchDeviceClass.SWITCH,
|
||||
),
|
||||
monitor=monitor,
|
||||
)
|
||||
for monitor in new_monitors
|
||||
]
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
|
||||
_check_device()
|
||||
entry.async_on_unload(coordinator.async_add_listener(_check_device))
|
||||
entry.async_on_unload(new_device_listener(coordinator, _add_new_entities))
|
||||
|
||||
|
||||
class UptimeRobotSwitch(UptimeRobotEntity, SwitchEntity):
|
||||
|
34
homeassistant/components/uptimerobot/utils.py
Normal file
34
homeassistant/components/uptimerobot/utils.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""Utility functions for the UptimeRobot integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from pyuptimerobot import UptimeRobotMonitor
|
||||
|
||||
from .coordinator import UptimeRobotDataUpdateCoordinator
|
||||
|
||||
|
||||
def new_device_listener(
|
||||
coordinator: UptimeRobotDataUpdateCoordinator,
|
||||
new_devices_callback: Callable[[list[UptimeRobotMonitor]], None],
|
||||
) -> Callable[[], None]:
|
||||
"""Subscribe to coordinator updates to check for new devices."""
|
||||
known_devices: set[int] = set()
|
||||
|
||||
def _check_devices() -> None:
|
||||
"""Check for new devices and call callback with any new monitors."""
|
||||
if not coordinator.data:
|
||||
return
|
||||
|
||||
new_monitors: list[UptimeRobotMonitor] = []
|
||||
for monitor in coordinator.data:
|
||||
if monitor.id not in known_devices:
|
||||
known_devices.add(monitor.id)
|
||||
new_monitors.append(monitor)
|
||||
|
||||
if new_monitors:
|
||||
new_devices_callback(new_monitors)
|
||||
|
||||
# Check for devices immediately
|
||||
_check_devices()
|
||||
|
||||
return coordinator.async_add_listener(_check_devices)
|
@@ -228,7 +228,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_SOURCE_SENSOR: source_entity_id},
|
||||
)
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
||||
entry.async_on_unload(
|
||||
async_handle_source_entity_changes(
|
||||
@@ -259,9 +258,17 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry, (Platform.SELECT, Platform.SENSOR)
|
||||
)
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(config_entry_update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Update listener, called when the config entry options are changed."""
|
||||
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
platforms_to_unload = [Platform.SENSOR]
|
||||
|
@@ -134,7 +134,6 @@ class ConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from holidays import HolidayBase
|
||||
|
||||
@@ -15,6 +15,8 @@ from . import WorkdayConfigEntry
|
||||
from .const import CONF_EXCLUDES, CONF_OFFSET, CONF_WORKDAYS
|
||||
from .entity import BaseWorkdayEntity
|
||||
|
||||
CALENDAR_DAYS_AHEAD = 365
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
@@ -71,10 +73,8 @@ class WorkdayCalendarEntity(BaseWorkdayEntity, CalendarEntity):
|
||||
def update_data(self, now: datetime) -> None:
|
||||
"""Update data."""
|
||||
event_list = []
|
||||
start_date = date(now.year, 1, 1)
|
||||
end_number_of_days = date(now.year + 1, 12, 31) - start_date
|
||||
for i in range(end_number_of_days.days + 1):
|
||||
future_date = start_date + timedelta(days=i)
|
||||
for i in range(CALENDAR_DAYS_AHEAD):
|
||||
future_date = now.date() + timedelta(days=i)
|
||||
if self.date_is_workday(future_date):
|
||||
event = CalendarEvent(
|
||||
summary=self._name,
|
||||
|
@@ -10,6 +10,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Worldclock 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
|
||||
|
||||
@@ -17,3 +18,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload World clock 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)
|
||||
|
@@ -97,7 +97,6 @@ class WorldclockConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
options_flow_reloads = True
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, Any]) -> str:
|
||||
"""Return config entry title."""
|
||||
|
@@ -1273,7 +1273,6 @@ class SelectSelectorConfig(BaseSelectorConfig, total=False):
|
||||
custom_value: bool
|
||||
mode: SelectSelectorMode
|
||||
translation_key: str
|
||||
translation_domain: str
|
||||
sort: bool
|
||||
|
||||
|
||||
@@ -1292,7 +1291,6 @@ class SelectSelector(Selector[SelectSelectorConfig]):
|
||||
vol.Coerce(SelectSelectorMode), lambda val: val.value
|
||||
),
|
||||
vol.Optional("translation_key"): cv.string,
|
||||
vol.Optional("translation_domain"): cv.string,
|
||||
vol.Optional("sort", default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -782,7 +782,7 @@ decora-wifi==1.4
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==15.0.0
|
||||
deebot-client==14.0.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -682,7 +682,7 @@ debugpy==1.8.16
|
||||
# decora==0.6
|
||||
|
||||
# homeassistant.components.ecovacs
|
||||
deebot-client==15.0.0
|
||||
deebot-client==14.0.0
|
||||
|
||||
# homeassistant.components.ihc
|
||||
# homeassistant.components.namecheapdns
|
||||
|
@@ -24,9 +24,6 @@ from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
NEW_PASSWORD = "new_password"
|
||||
REAUTH_STEP = "reauth_confirm"
|
||||
|
||||
MOCK_CONFIG = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: "ubnt",
|
||||
@@ -36,11 +33,6 @@ MOCK_CONFIG = {
|
||||
CONF_VERIFY_SSL: False,
|
||||
},
|
||||
}
|
||||
MOCK_CONFIG_REAUTH = {
|
||||
CONF_HOST: "1.1.1.1",
|
||||
CONF_USERNAME: "ubnt",
|
||||
CONF_PASSWORD: "wrong-password",
|
||||
}
|
||||
|
||||
|
||||
async def test_form_creates_entry(
|
||||
@@ -97,6 +89,7 @@ async def test_form_duplicate_entry(
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
||||
(AirOSDeviceConnectionError, "cannot_connect"),
|
||||
(AirOSKeyDataMissingError, "key_data_missing"),
|
||||
(Exception, "unknown"),
|
||||
@@ -135,95 +128,3 @@ async def test_form_exception_handling(
|
||||
assert result["title"] == "NanoStation 5AC ap name"
|
||||
assert result["data"] == MOCK_CONFIG
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("reauth_exception", "expected_error"),
|
||||
[
|
||||
(None, None),
|
||||
(AirOSConnectionAuthenticationError, "invalid_auth"),
|
||||
(AirOSDeviceConnectionError, "cannot_connect"),
|
||||
(AirOSKeyDataMissingError, "key_data_missing"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
ids=[
|
||||
"reauth_succes",
|
||||
"invalid_auth",
|
||||
"cannot_connect",
|
||||
"key_data_missing",
|
||||
"unknown",
|
||||
],
|
||||
)
|
||||
async def test_reauth_flow_scenarios(
|
||||
hass: HomeAssistant,
|
||||
mock_airos_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
reauth_exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test reauthentication from start (failure) to finish (success)."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow["step_id"] == REAUTH_STEP
|
||||
|
||||
mock_airos_client.login.side_effect = reauth_exception
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow["flow_id"],
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
|
||||
if expected_error:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == REAUTH_STEP
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
|
||||
# Retry
|
||||
mock_airos_client.login.side_effect = None
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow["flow_id"],
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
|
||||
# Always test resolution
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert updated_entry.data[CONF_PASSWORD] == NEW_PASSWORD
|
||||
|
||||
|
||||
async def test_reauth_unique_id_mismatch(
|
||||
hass: HomeAssistant,
|
||||
mock_airos_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reauthentication failure when the unique ID changes."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_airos_client.login.side_effect = AirOSConnectionAuthenticationError
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
flow = flows[0]
|
||||
|
||||
mock_airos_client.login.side_effect = None
|
||||
mock_airos_client.status.return_value.derived.mac = "FF:23:45:67:89:AB"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
flow["flow_id"],
|
||||
user_input={CONF_PASSWORD: NEW_PASSWORD},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
|
||||
updated_entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id)
|
||||
assert updated_entry.data[CONF_PASSWORD] != NEW_PASSWORD
|
||||
|
@@ -3,7 +3,11 @@
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from airos.exceptions import AirOSDataMissingError, AirOSDeviceConnectionError
|
||||
from airos.exceptions import (
|
||||
AirOSConnectionAuthenticationError,
|
||||
AirOSDataMissingError,
|
||||
AirOSDeviceConnectionError,
|
||||
)
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
@@ -35,6 +39,7 @@ async def test_all_entities(
|
||||
@pytest.mark.parametrize(
|
||||
("exception"),
|
||||
[
|
||||
AirOSConnectionAuthenticationError,
|
||||
TimeoutError,
|
||||
AirOSDeviceConnectionError,
|
||||
AirOSDataMissingError,
|
||||
|
@@ -29,12 +29,12 @@ RECEIVER_INFO_2 = ReceiverInfo(
|
||||
def mock_discovery(receiver_infos: Iterable[ReceiverInfo] | None) -> Generator[None]:
|
||||
"""Mock discovery functions."""
|
||||
|
||||
async def get_info(host: str) -> ReceiverInfo:
|
||||
async def get_info(host: str) -> ReceiverInfo | None:
|
||||
"""Get receiver info by host."""
|
||||
for info in receiver_infos:
|
||||
if info.host == host:
|
||||
return info
|
||||
raise TimeoutError
|
||||
return None
|
||||
|
||||
def get_infos(host: str) -> MagicMock:
|
||||
"""Get receiver infos from broadcast."""
|
||||
|
@@ -49,14 +49,14 @@ async def test_all_entities(
|
||||
PortainerTimeoutError("timeout"),
|
||||
],
|
||||
)
|
||||
async def test_refresh_endpoints_exceptions(
|
||||
async def test_refresh_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_portainer_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test entities go unavailable after coordinator refresh failures, for the endpoint fetch."""
|
||||
"""Test entities go unavailable after coordinator refresh failures."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
@@ -69,26 +69,8 @@ async def test_refresh_endpoints_exceptions(
|
||||
state = hass.states.get("binary_sensor.practical_morse_status")
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception"),
|
||||
[
|
||||
PortainerAuthenticationError("bad creds"),
|
||||
PortainerConnectionError("cannot connect"),
|
||||
PortainerTimeoutError("timeout"),
|
||||
],
|
||||
)
|
||||
async def test_refresh_containers_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_portainer_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
exception: Exception,
|
||||
) -> None:
|
||||
"""Test entities go unavailable after coordinator refresh failures, for the container fetch."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Reset endpoints; fail on containers fetch
|
||||
mock_portainer_client.get_endpoints.side_effect = None
|
||||
mock_portainer_client.get_containers.side_effect = exception
|
||||
|
||||
freezer.tick(DEFAULT_SCAN_INTERVAL)
|
||||
|
@@ -126,99 +126,3 @@ async def test_duplicate_entry(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_full_flow_reauth(
|
||||
hass: HomeAssistant,
|
||||
mock_portainer_client: AsyncMock,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test the full flow of the config flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
# There is no user input
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_API_TOKEN: "new_api_key"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data[CONF_API_TOKEN] == "new_api_key"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "reason"),
|
||||
[
|
||||
(
|
||||
PortainerAuthenticationError,
|
||||
"invalid_auth",
|
||||
),
|
||||
(
|
||||
PortainerConnectionError,
|
||||
"cannot_connect",
|
||||
),
|
||||
(
|
||||
PortainerTimeoutError,
|
||||
"timeout_connect",
|
||||
),
|
||||
(
|
||||
Exception("Some other error"),
|
||||
"unknown",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_reauth_flow_exceptions(
|
||||
hass: HomeAssistant,
|
||||
mock_portainer_client: AsyncMock,
|
||||
mock_setup_entry: MagicMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Test we handle all exceptions in the reauth flow."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
mock_portainer_client.get_endpoints.side_effect = exception
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await mock_config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_API_TOKEN: "new_api_key"},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": reason}
|
||||
|
||||
# Now test that we can recover from the error
|
||||
mock_portainer_client.get_endpoints.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_API_TOKEN: "new_api_key"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert mock_config_entry.data[CONF_API_TOKEN] == "new_api_key"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
@@ -1,13 +1,9 @@
|
||||
"""Tests for sensor helpers."""
|
||||
"""The test for sensor helpers."""
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN, SensorDeviceClass, SensorStateClass
|
||||
from homeassistant.components.sensor.helpers import (
|
||||
async_parse_date_datetime,
|
||||
create_sensor_device_class_select_selector,
|
||||
create_sensor_state_class_select_selector,
|
||||
)
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor.helpers import async_parse_date_datetime
|
||||
|
||||
|
||||
def test_async_parse_datetime(caplog: pytest.LogCaptureFixture) -> None:
|
||||
@@ -43,23 +39,3 @@ def test_async_parse_datetime(caplog: pytest.LogCaptureFixture) -> None:
|
||||
# Invalid date
|
||||
assert async_parse_date_datetime("December 12th", entity_id, device_class) is None
|
||||
assert "sensor.timestamp rendered invalid date December 12th" in caplog.text
|
||||
|
||||
|
||||
def test_create_sensor_device_class_select_selector() -> None:
|
||||
"Test Create sensor state class select selector helper."
|
||||
selector = create_sensor_device_class_select_selector()
|
||||
assert selector.config["options"] == list(SensorDeviceClass)
|
||||
assert selector.config["translation_domain"] == DOMAIN
|
||||
assert selector.config["translation_key"] == "device_class"
|
||||
assert selector.config["sort"]
|
||||
assert not selector.config["custom_value"]
|
||||
|
||||
|
||||
def test_create_sensor_state_class_select_selector() -> None:
|
||||
"Test Create sensor state class select selector helper."
|
||||
selector = create_sensor_state_class_select_selector()
|
||||
assert selector.config["options"] == list(SensorStateClass)
|
||||
assert selector.config["translation_domain"] == DOMAIN
|
||||
assert selector.config["translation_key"] == "state_class"
|
||||
assert selector.config["sort"]
|
||||
assert not selector.config["custom_value"]
|
||||
|
@@ -136,8 +136,7 @@ async def test_options_form(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def test_user_form_timeout(hass: HomeAssistant) -> None:
|
||||
"""Test we handle server search timeout and allow manual entry."""
|
||||
# First flow: simulate timeout
|
||||
"""Test we handle server search timeout."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.squeezebox.config_flow.async_discover",
|
||||
@@ -151,46 +150,16 @@ async def test_user_form_timeout(hass: HomeAssistant) -> None:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_server_found"}
|
||||
|
||||
# Second flow: simulate successful discovery
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.squeezebox.config_flow.async_discover",
|
||||
mock_discover,
|
||||
),
|
||||
patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.squeezebox.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
# simulate manual input of host
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_HOST: HOST2}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["data"] == {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
}
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "edit"
|
||||
assert CONF_HOST in result2["data_schema"].schema
|
||||
for key in result2["data_schema"].schema:
|
||||
if key == CONF_HOST:
|
||||
assert key.description == {"suggested_value": HOST2}
|
||||
|
||||
|
||||
async def test_user_form_duplicate(hass: HomeAssistant) -> None:
|
||||
@@ -347,15 +316,11 @@ async def test_form_validate_exception(hass: HomeAssistant) -> None:
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error, then succeed after retry."""
|
||||
|
||||
# Start the flow
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "edit"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
|
||||
# First attempt: simulate cannot connect
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value=False,
|
||||
@@ -365,47 +330,17 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_USERNAME: "test-username",
|
||||
CONF_PASSWORD: "test-password",
|
||||
},
|
||||
)
|
||||
|
||||
# We should still be in a form, with an error
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
# Second attempt: simulate a successful connection
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST # the flow uses host as title
|
||||
assert result["data"] == {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
}
|
||||
assert result["context"]["unique_id"] == UUID
|
||||
|
||||
|
||||
async def test_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test handling of discovered server, then completing the flow."""
|
||||
|
||||
# Initial discovery: server responds with a uuid
|
||||
"""Test handling of discovered server."""
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
@@ -415,109 +350,24 @@ async def test_discovery(hass: HomeAssistant) -> None:
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_HOST: HOST, CONF_PORT: PORT, "uuid": UUID},
|
||||
)
|
||||
|
||||
# Discovery puts us into the edit step
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
# Complete the edit step with user input
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
# Flow should now complete with a config entry
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["data"] == {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
}
|
||||
assert result["context"]["unique_id"] == UUID
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
|
||||
async def test_discovery_no_uuid(hass: HomeAssistant) -> None:
|
||||
"""Test discovery without uuid first fails, then succeeds when uuid is available."""
|
||||
|
||||
# Initial discovery: no uuid returned
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
new=patch_async_query_unauthorized,
|
||||
):
|
||||
"""Test handling of discovered server with unavailable uuid."""
|
||||
with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_INTEGRATION_DISCOVERY},
|
||||
data={CONF_HOST: HOST, CONF_PORT: PORT, CONF_HTTPS: False},
|
||||
)
|
||||
|
||||
# Flow shows the edit form
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
# First attempt to complete: still no uuid → error on the form
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
new=patch_async_query_unauthorized,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_auth"}
|
||||
|
||||
# Second attempt: now the server responds with a uuid
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
# Flow should now complete successfully
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["data"] == {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
}
|
||||
assert result["context"]["unique_id"] == UUID
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
|
||||
async def test_dhcp_discovery(hass: HomeAssistant) -> None:
|
||||
"""Test we can process discovery from dhcp and complete the flow."""
|
||||
|
||||
"""Test we can process discovery from dhcp."""
|
||||
with (
|
||||
patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
@@ -532,48 +382,17 @@ async def test_dhcp_discovery(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=DhcpServiceInfo(
|
||||
ip=HOST,
|
||||
ip="1.1.1.1",
|
||||
macaddress="aabbccddeeff",
|
||||
hostname="any",
|
||||
),
|
||||
)
|
||||
|
||||
# DHCP discovery puts us into the edit step
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
# Complete the edit step with user input
|
||||
with patch(
|
||||
"pysqueezebox.Server.async_query",
|
||||
return_value={"uuid": UUID},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
# Flow should now complete with a config entry
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == HOST
|
||||
assert result["data"] == {
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
}
|
||||
assert result["context"]["unique_id"] == UUID
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
|
||||
async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None:
|
||||
"""Test we can handle dhcp discovery when no server is found."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.squeezebox.config_flow.async_discover",
|
||||
@@ -585,43 +404,13 @@ async def test_dhcp_discovery_no_server_found(hass: HomeAssistant) -> None:
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_DHCP},
|
||||
data=DhcpServiceInfo(
|
||||
ip=HOST,
|
||||
ip="1.1.1.1",
|
||||
macaddress="aabbccddeeff",
|
||||
hostname="any",
|
||||
),
|
||||
)
|
||||
|
||||
# First step: user form with only host
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
# Provide just the host to move into edit step
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_HOST: HOST},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
|
||||
# Now try to complete the edit step with full schema
|
||||
with patch(
|
||||
"homeassistant.components.squeezebox.config_flow.async_discover",
|
||||
mock_failed_discover,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_HOST: HOST,
|
||||
CONF_PORT: PORT,
|
||||
CONF_USERNAME: "",
|
||||
CONF_PASSWORD: "",
|
||||
CONF_HTTPS: False,
|
||||
},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "edit"
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
|
||||
async def test_dhcp_discovery_existing_player(
|
||||
|
@@ -1,16 +0,0 @@
|
||||
"""Tests for switch helpers."""
|
||||
|
||||
from homeassistant.components.switch import DOMAIN, SwitchDeviceClass
|
||||
from homeassistant.components.switch.helpers import (
|
||||
create_switch_device_class_select_selector,
|
||||
)
|
||||
|
||||
|
||||
def test_create_switch_device_class_select_selector() -> None:
|
||||
"Test Create sensor state class select selector helper."
|
||||
selector = create_switch_device_class_select_selector()
|
||||
assert selector.config["options"] == list(SwitchDeviceClass)
|
||||
assert selector.config["translation_domain"] == DOMAIN
|
||||
assert selector.config["translation_key"] == "device_class"
|
||||
assert selector.config["sort"]
|
||||
assert not selector.config["custom_value"]
|
@@ -57,18 +57,9 @@ def mock_client(enable_bluetooth: None, mock_client_class: Mock) -> Generator[Mo
|
||||
client_object.mocked_notify = None
|
||||
|
||||
async def _connect(
|
||||
address: str,
|
||||
callback: Callable[[Packet], None] | None = None,
|
||||
disconnected_callback: Callable[[], None] | None = None,
|
||||
address: str, callback: Callable[[Packet], None] | None = None
|
||||
) -> Mock:
|
||||
client_object.mocked_notify = callback
|
||||
if disconnected_callback:
|
||||
|
||||
def _disconnected_callback():
|
||||
client_object.is_connected = False
|
||||
disconnected_callback()
|
||||
|
||||
client_object.mocked_disconnected_callback = _disconnected_callback
|
||||
return client_object
|
||||
|
||||
async def _disconnect() -> None:
|
||||
|
@@ -1,8 +1,7 @@
|
||||
"""Test sensors for ToGrill integration."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from unittest.mock import Mock
|
||||
|
||||
from habluetooth import BluetoothServiceInfoBleak
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from togrill_bluetooth.packets import PacketA0Notify, PacketA1Notify
|
||||
@@ -17,16 +16,6 @@ from tests.common import MockConfigEntry, snapshot_platform
|
||||
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||
|
||||
|
||||
def patch_async_ble_device_from_address(
|
||||
return_value: BluetoothServiceInfoBleak | None = None,
|
||||
):
|
||||
"""Patch async_ble_device_from_address to return a mocked BluetoothServiceInfoBleak."""
|
||||
return patch(
|
||||
"homeassistant.components.bluetooth.async_ble_device_from_address",
|
||||
return_value=return_value,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"packets",
|
||||
[
|
||||
@@ -68,51 +57,3 @@ async def test_setup(
|
||||
mock_client.mocked_notify(packet)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_entry.entry_id)
|
||||
|
||||
|
||||
async def test_device_disconnected(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockConfigEntry,
|
||||
mock_client: Mock,
|
||||
) -> None:
|
||||
"""Test the switch set."""
|
||||
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
|
||||
|
||||
await setup_entry(hass, mock_entry, [Platform.SENSOR])
|
||||
|
||||
entity_id = "sensor.pro_05_battery"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
|
||||
with patch_async_ble_device_from_address():
|
||||
mock_client.mocked_disconnected_callback()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
|
||||
async def test_device_discovered(
|
||||
hass: HomeAssistant,
|
||||
mock_entry: MockConfigEntry,
|
||||
mock_client: Mock,
|
||||
) -> None:
|
||||
"""Test the switch set."""
|
||||
|
||||
await setup_entry(hass, mock_entry, [Platform.SENSOR])
|
||||
|
||||
entity_id = "sensor.pro_05_battery"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "unavailable"
|
||||
|
||||
inject_bluetooth_service_info(hass, TOGRILL_SERVICE_INFO)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
|
@@ -721,7 +721,6 @@ def test_text_selector_schema(schema, valid_selections, invalid_selections) -> N
|
||||
{
|
||||
"options": ["red", "green", "blue"],
|
||||
"translation_key": "color",
|
||||
"translation_domain": "homeassistant",
|
||||
},
|
||||
("red", "green", "blue"),
|
||||
("cat", 0, None, ["red"]),
|
||||
|
Reference in New Issue
Block a user