mirror of
https://github.com/home-assistant/core.git
synced 2025-09-08 06:11:31 +02:00
Baysesian Config Flow (#122552)
Co-authored-by: G Johansson <goran.johansson@shiftit.se> Co-authored-by: Norbert Rittel <norbert@rittel.de> Co-authored-by: Erik Montnemery <erik@montnemery.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,24 @@
|
||||
"""The bayesian component."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
DOMAIN = "bayesian"
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
from .const import PLATFORMS
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Bayesian from a config entry."""
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload Bayesian config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update."""
|
||||
hass.config_entries.async_schedule_reload(entry.entry_id)
|
||||
|
@@ -16,6 +16,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
@@ -32,7 +33,10 @@ from homeassistant.const import (
|
||||
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConditionError, TemplateError
|
||||
from homeassistant.helpers import condition, config_validation as cv
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
AddConfigEntryEntitiesCallback,
|
||||
AddEntitiesCallback,
|
||||
)
|
||||
from homeassistant.helpers.event import (
|
||||
TrackTemplate,
|
||||
TrackTemplateResult,
|
||||
@@ -44,7 +48,6 @@ from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.template import Template, result_as_boolean
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from . import DOMAIN, PLATFORMS
|
||||
from .const import (
|
||||
ATTR_OBSERVATIONS,
|
||||
ATTR_OCCURRED_OBSERVATION_ENTITIES,
|
||||
@@ -60,6 +63,8 @@ from .const import (
|
||||
CONF_TO_STATE,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PROBABILITY_THRESHOLD,
|
||||
DOMAIN,
|
||||
PLATFORMS,
|
||||
)
|
||||
from .helpers import Observation
|
||||
from .issues import raise_mirrored_entries, raise_no_prob_given_false
|
||||
@@ -67,7 +72,13 @@ from .issues import raise_mirrored_entries, raise_no_prob_given_false
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
|
||||
def above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Validate above and below options.
|
||||
|
||||
If the observation is of type/platform NUMERIC_STATE, then ensure that the
|
||||
value given for 'above' is not greater than that for 'below'. Also check
|
||||
that at least one of the two is specified.
|
||||
"""
|
||||
if config[CONF_PLATFORM] == CONF_NUMERIC_STATE:
|
||||
above = config.get(CONF_ABOVE)
|
||||
below = config.get(CONF_BELOW)
|
||||
@@ -76,9 +87,7 @@ def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
|
||||
"For bayesian numeric state for entity: %s at least one of 'above' or 'below' must be specified",
|
||||
config[CONF_ENTITY_ID],
|
||||
)
|
||||
raise vol.Invalid(
|
||||
"For bayesian numeric state at least one of 'above' or 'below' must be specified."
|
||||
)
|
||||
raise vol.Invalid("above_or_below")
|
||||
if above is not None and below is not None:
|
||||
if above > below:
|
||||
_LOGGER.error(
|
||||
@@ -86,7 +95,7 @@ def _above_greater_than_below(config: dict[str, Any]) -> dict[str, Any]:
|
||||
above,
|
||||
below,
|
||||
)
|
||||
raise vol.Invalid("'above' is greater than 'below'")
|
||||
raise vol.Invalid("above_below")
|
||||
return config
|
||||
|
||||
|
||||
@@ -102,11 +111,16 @@ NUMERIC_STATE_SCHEMA = vol.All(
|
||||
},
|
||||
required=True,
|
||||
),
|
||||
_above_greater_than_below,
|
||||
above_greater_than_below,
|
||||
)
|
||||
|
||||
|
||||
def _no_overlapping(configs: list[dict]) -> list[dict]:
|
||||
def no_overlapping(configs: list[dict]) -> list[dict]:
|
||||
"""Validate that intervals are not overlapping.
|
||||
|
||||
For a list of observations ensure that there are no overlapping intervals
|
||||
for NUMERIC_STATE observations for the same entity.
|
||||
"""
|
||||
numeric_configs = [
|
||||
config for config in configs if config[CONF_PLATFORM] == CONF_NUMERIC_STATE
|
||||
]
|
||||
@@ -129,11 +143,16 @@ def _no_overlapping(configs: list[dict]) -> list[dict]:
|
||||
|
||||
for i, tup in enumerate(intervals):
|
||||
if len(intervals) > i + 1 and tup.below > intervals[i + 1].above:
|
||||
_LOGGER.error(
|
||||
"Ranges for bayesian numeric state entities must not overlap, but %s has overlapping ranges, above:%s, below:%s overlaps with above:%s, below:%s",
|
||||
ent_id,
|
||||
tup.above,
|
||||
tup.below,
|
||||
intervals[i + 1].above,
|
||||
intervals[i + 1].below,
|
||||
)
|
||||
raise vol.Invalid(
|
||||
"Ranges for bayesian numeric state entities must not overlap, "
|
||||
f"but {ent_id} has overlapping ranges, above:{tup.above}, "
|
||||
f"below:{tup.below} overlaps with above:{intervals[i + 1].above}, "
|
||||
f"below:{intervals[i + 1].below}."
|
||||
"overlapping_ranges",
|
||||
)
|
||||
return configs
|
||||
|
||||
@@ -168,7 +187,7 @@ PLATFORM_SCHEMA = BINARY_SENSOR_PLATFORM_SCHEMA.extend(
|
||||
vol.All(
|
||||
cv.ensure_list,
|
||||
[vol.Any(TEMPLATE_SCHEMA, STATE_SCHEMA, NUMERIC_STATE_SCHEMA)],
|
||||
_no_overlapping,
|
||||
no_overlapping,
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_PRIOR): vol.Coerce(float),
|
||||
@@ -194,9 +213,13 @@ async def async_setup_platform(
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Bayesian Binary sensor."""
|
||||
"""Set up the Bayesian Binary sensor from a yaml config."""
|
||||
_LOGGER.debug(
|
||||
"Setting up config entry for Bayesian sensor: '%s' with %s observations",
|
||||
config[CONF_NAME],
|
||||
len(config.get(CONF_OBSERVATIONS, [])),
|
||||
)
|
||||
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
|
||||
|
||||
name: str = config[CONF_NAME]
|
||||
unique_id: str | None = config.get(CONF_UNIQUE_ID)
|
||||
observations: list[ConfigType] = config[CONF_OBSERVATIONS]
|
||||
@@ -231,6 +254,42 @@ async def async_setup_platform(
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the Bayesian Binary sensor from a config entry."""
|
||||
_LOGGER.debug(
|
||||
"Setting up config entry for Bayesian sensor: '%s' with %s observations",
|
||||
config_entry.options[CONF_NAME],
|
||||
len(config_entry.subentries),
|
||||
)
|
||||
config = config_entry.options
|
||||
name: str = config[CONF_NAME]
|
||||
unique_id: str | None = config.get(CONF_UNIQUE_ID, config_entry.entry_id)
|
||||
observations: list[ConfigType] = [
|
||||
dict(subentry.data) for subentry in config_entry.subentries.values()
|
||||
]
|
||||
prior: float = config[CONF_PRIOR]
|
||||
probability_threshold: float = config[CONF_PROBABILITY_THRESHOLD]
|
||||
device_class: BinarySensorDeviceClass | None = config.get(CONF_DEVICE_CLASS)
|
||||
|
||||
async_add_entities(
|
||||
[
|
||||
BayesianBinarySensor(
|
||||
name,
|
||||
unique_id,
|
||||
prior,
|
||||
observations,
|
||||
probability_threshold,
|
||||
device_class,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BayesianBinarySensor(BinarySensorEntity):
|
||||
"""Representation of a Bayesian sensor."""
|
||||
|
||||
@@ -248,6 +307,7 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
"""Initialize the Bayesian sensor."""
|
||||
self._attr_name = name
|
||||
self._attr_unique_id = unique_id and f"bayesian-{unique_id}"
|
||||
|
||||
self._observations = [
|
||||
Observation(
|
||||
entity_id=observation.get(CONF_ENTITY_ID),
|
||||
@@ -432,7 +492,7 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
1 - observation.prob_given_false,
|
||||
)
|
||||
continue
|
||||
# observation.observed is None
|
||||
# Entity exists but observation.observed is None
|
||||
if observation.entity_id is not None:
|
||||
_LOGGER.debug(
|
||||
(
|
||||
@@ -495,7 +555,10 @@ class BayesianBinarySensor(BinarySensorEntity):
|
||||
for observation in self._observations:
|
||||
if observation.value_template is None:
|
||||
continue
|
||||
|
||||
if isinstance(observation.value_template, str):
|
||||
observation.value_template = Template(
|
||||
observation.value_template, hass=self.hass
|
||||
)
|
||||
template = observation.value_template
|
||||
observations_by_template.setdefault(template, []).append(observation)
|
||||
|
||||
|
646
homeassistant/components/bayesian/config_flow.py
Normal file
646
homeassistant/components/bayesian/config_flow.py
Normal file
@@ -0,0 +1,646 @@
|
||||
"""Config flow for the Bayesian integration."""
|
||||
|
||||
from collections.abc import Mapping
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
)
|
||||
from homeassistant.components.calendar import DOMAIN as CALENDAR_DOMAIN
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.components.cover import DOMAIN as COVER_DOMAIN
|
||||
from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER_DOMAIN
|
||||
from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN
|
||||
from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN
|
||||
from homeassistant.components.input_text import DOMAIN as INPUT_TEXT_DOMAIN
|
||||
from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||
from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN
|
||||
from homeassistant.components.person import DOMAIN as PERSON_DOMAIN
|
||||
from homeassistant.components.select import DOMAIN as SELECT_DOMAIN
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sun import DOMAIN as SUN_DOMAIN
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||
from homeassistant.components.update import DOMAIN as UPDATE_DOMAIN
|
||||
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
|
||||
from homeassistant.components.zone import DOMAIN as ZONE_DOMAIN
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlowResult,
|
||||
ConfigSubentry,
|
||||
ConfigSubentryData,
|
||||
ConfigSubentryFlow,
|
||||
SubentryFlowResult,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_BELOW,
|
||||
CONF_DEVICE_CLASS,
|
||||
CONF_ENTITY_ID,
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_STATE,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import selector, translation
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
SchemaCommonFlowHandler,
|
||||
SchemaConfigFlowHandler,
|
||||
SchemaFlowError,
|
||||
SchemaFlowFormStep,
|
||||
SchemaFlowMenuStep,
|
||||
)
|
||||
|
||||
from .binary_sensor import above_greater_than_below, no_overlapping
|
||||
from .const import (
|
||||
CONF_OBSERVATIONS,
|
||||
CONF_P_GIVEN_F,
|
||||
CONF_P_GIVEN_T,
|
||||
CONF_PRIOR,
|
||||
CONF_PROBABILITY_THRESHOLD,
|
||||
CONF_TEMPLATE,
|
||||
CONF_TO_STATE,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_PROBABILITY_THRESHOLD,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
USER = "user"
|
||||
OBSERVATION_SELECTOR = "observation_selector"
|
||||
ALLOWED_STATE_DOMAINS = [
|
||||
ALARM_DOMAIN,
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
CALENDAR_DOMAIN,
|
||||
CLIMATE_DOMAIN,
|
||||
COVER_DOMAIN,
|
||||
DEVICE_TRACKER_DOMAIN,
|
||||
INPUT_BOOLEAN_DOMAIN,
|
||||
INPUT_NUMBER_DOMAIN,
|
||||
INPUT_TEXT_DOMAIN,
|
||||
LIGHT_DOMAIN,
|
||||
MEDIA_PLAYER_DOMAIN,
|
||||
NOTIFY_DOMAIN,
|
||||
NUMBER_DOMAIN,
|
||||
PERSON_DOMAIN,
|
||||
"schedule", # Avoids an import that would introduce a dependency.
|
||||
SELECT_DOMAIN,
|
||||
SENSOR_DOMAIN,
|
||||
SUN_DOMAIN,
|
||||
SWITCH_DOMAIN,
|
||||
TODO_DOMAIN,
|
||||
UPDATE_DOMAIN,
|
||||
WEATHER_DOMAIN,
|
||||
]
|
||||
ALLOWED_NUMERIC_DOMAINS = [
|
||||
SENSOR_DOMAIN,
|
||||
INPUT_NUMBER_DOMAIN,
|
||||
NUMBER_DOMAIN,
|
||||
TODO_DOMAIN,
|
||||
ZONE_DOMAIN,
|
||||
]
|
||||
|
||||
|
||||
class ObservationTypes(StrEnum):
|
||||
"""StrEnum for all the different observation types."""
|
||||
|
||||
STATE = CONF_STATE
|
||||
NUMERIC_STATE = "numeric_state"
|
||||
TEMPLATE = CONF_TEMPLATE
|
||||
|
||||
|
||||
class OptionsFlowSteps(StrEnum):
|
||||
"""StrEnum for all the different options flow steps."""
|
||||
|
||||
INIT = "init"
|
||||
ADD_OBSERVATION = OBSERVATION_SELECTOR
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_PROBABILITY_THRESHOLD, default=DEFAULT_PROBABILITY_THRESHOLD * 100
|
||||
): vol.All(
|
||||
selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
step=1.0,
|
||||
min=0,
|
||||
max=100,
|
||||
unit_of_measurement="%",
|
||||
),
|
||||
),
|
||||
vol.Range(
|
||||
min=0,
|
||||
max=100,
|
||||
min_included=False,
|
||||
max_included=False,
|
||||
msg="extreme_threshold_error",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_PRIOR, default=DEFAULT_PROBABILITY_THRESHOLD * 100): vol.All(
|
||||
selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
step=1.0,
|
||||
min=0,
|
||||
max=100,
|
||||
unit_of_measurement="%",
|
||||
),
|
||||
),
|
||||
vol.Range(
|
||||
min=0,
|
||||
max=100,
|
||||
min_included=False,
|
||||
max_included=False,
|
||||
msg="extreme_prior_error",
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=[cls.value for cls in BinarySensorDeviceClass],
|
||||
mode=selector.SelectSelectorMode.DROPDOWN,
|
||||
translation_key="binary_sensor_device_class",
|
||||
sort=True,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_NAME, default=DEFAULT_NAME): selector.TextSelector(),
|
||||
}
|
||||
).extend(OPTIONS_SCHEMA.schema)
|
||||
|
||||
OBSERVATION_BOILERPLATE = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_P_GIVEN_T): vol.All(
|
||||
selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
step=1.0,
|
||||
min=0,
|
||||
max=100,
|
||||
unit_of_measurement="%",
|
||||
),
|
||||
),
|
||||
vol.Range(
|
||||
min=0,
|
||||
max=100,
|
||||
min_included=False,
|
||||
max_included=False,
|
||||
msg="extreme_prob_given_error",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_P_GIVEN_F): vol.All(
|
||||
selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
step=1.0,
|
||||
min=0,
|
||||
max=100,
|
||||
unit_of_measurement="%",
|
||||
),
|
||||
),
|
||||
vol.Range(
|
||||
min=0,
|
||||
max=100,
|
||||
min_included=False,
|
||||
max_included=False,
|
||||
msg="extreme_prob_given_error",
|
||||
),
|
||||
),
|
||||
vol.Required(CONF_NAME): selector.TextSelector(),
|
||||
}
|
||||
)
|
||||
|
||||
STATE_SUBSCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=ALLOWED_STATE_DOMAINS)
|
||||
),
|
||||
vol.Required(CONF_TO_STATE): selector.TextSelector(
|
||||
selector.TextSelectorConfig(
|
||||
multiline=False, type=selector.TextSelectorType.TEXT, multiple=False
|
||||
) # ideally this would be a state selector context-linked to the above entity.
|
||||
),
|
||||
},
|
||||
).extend(OBSERVATION_BOILERPLATE.schema)
|
||||
|
||||
NUMERIC_STATE_SUBSCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ENTITY_ID): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=ALLOWED_NUMERIC_DOMAINS)
|
||||
),
|
||||
vol.Optional(CONF_ABOVE): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step="any"
|
||||
),
|
||||
),
|
||||
vol.Optional(CONF_BELOW): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
mode=selector.NumberSelectorMode.BOX, step="any"
|
||||
),
|
||||
),
|
||||
},
|
||||
).extend(OBSERVATION_BOILERPLATE.schema)
|
||||
|
||||
|
||||
TEMPLATE_SUBSCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_VALUE_TEMPLATE): selector.TemplateSelector(
|
||||
selector.TemplateSelectorConfig(),
|
||||
),
|
||||
},
|
||||
).extend(OBSERVATION_BOILERPLATE.schema)
|
||||
|
||||
|
||||
def _convert_percentages_to_fractions(
|
||||
data: dict[str, str | float | int],
|
||||
) -> dict[str, str | float]:
|
||||
"""Convert percentage probability values in a dictionary to fractions for storing in the config entry."""
|
||||
probabilities = [
|
||||
CONF_P_GIVEN_T,
|
||||
CONF_P_GIVEN_F,
|
||||
CONF_PRIOR,
|
||||
CONF_PROBABILITY_THRESHOLD,
|
||||
]
|
||||
return {
|
||||
key: (
|
||||
value / 100
|
||||
if isinstance(value, (int, float)) and key in probabilities
|
||||
else value
|
||||
)
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
|
||||
def _convert_fractions_to_percentages(
|
||||
data: dict[str, str | float],
|
||||
) -> dict[str, str | float]:
|
||||
"""Convert fraction probability values in a dictionary to percentages for loading into the UI."""
|
||||
probabilities = [
|
||||
CONF_P_GIVEN_T,
|
||||
CONF_P_GIVEN_F,
|
||||
CONF_PRIOR,
|
||||
CONF_PROBABILITY_THRESHOLD,
|
||||
]
|
||||
return {
|
||||
key: (
|
||||
value * 100
|
||||
if isinstance(value, (int, float)) and key in probabilities
|
||||
else value
|
||||
)
|
||||
for key, value in data.items()
|
||||
}
|
||||
|
||||
|
||||
def _select_observation_schema(
|
||||
obs_type: ObservationTypes,
|
||||
) -> vol.Schema:
|
||||
"""Return the schema for editing the correct observation (SubEntry) type."""
|
||||
if obs_type == str(ObservationTypes.STATE):
|
||||
return STATE_SUBSCHEMA
|
||||
if obs_type == str(ObservationTypes.NUMERIC_STATE):
|
||||
return NUMERIC_STATE_SUBSCHEMA
|
||||
|
||||
return TEMPLATE_SUBSCHEMA
|
||||
|
||||
|
||||
async def _get_base_suggested_values(
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> dict[str, Any]:
|
||||
"""Return suggested values for the base sensor options."""
|
||||
|
||||
return _convert_fractions_to_percentages(dict(handler.options))
|
||||
|
||||
|
||||
def _get_observation_values_for_editing(
|
||||
subentry: ConfigSubentry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return the values for editing in the observation subentry."""
|
||||
|
||||
return _convert_fractions_to_percentages(dict(subentry.data))
|
||||
|
||||
|
||||
async def _validate_user(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
"""Modify user input to convert to fractions for storage. Validation is done entirely by the schemas."""
|
||||
user_input = _convert_percentages_to_fractions(user_input)
|
||||
return {**user_input}
|
||||
|
||||
|
||||
def _validate_observation_subentry(
|
||||
obs_type: ObservationTypes,
|
||||
user_input: dict[str, Any],
|
||||
other_subentries: list[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate an observation input and manually update options with observations as they are nested items."""
|
||||
|
||||
if user_input[CONF_P_GIVEN_T] == user_input[CONF_P_GIVEN_F]:
|
||||
raise SchemaFlowError("equal_probabilities")
|
||||
user_input = _convert_percentages_to_fractions(user_input)
|
||||
|
||||
# Save the observation type in the user input as it is needed in binary_sensor.py
|
||||
user_input[CONF_PLATFORM] = str(obs_type)
|
||||
|
||||
# Additional validation for multiple numeric state observations
|
||||
if (
|
||||
user_input[CONF_PLATFORM] == ObservationTypes.NUMERIC_STATE
|
||||
and other_subentries is not None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"Comparing with other subentries: %s", [*other_subentries, user_input]
|
||||
)
|
||||
try:
|
||||
above_greater_than_below(user_input)
|
||||
no_overlapping([*other_subentries, user_input])
|
||||
except vol.Invalid as err:
|
||||
raise SchemaFlowError(err) from err
|
||||
|
||||
_LOGGER.debug("Processed observation with settings: %s", user_input)
|
||||
return user_input
|
||||
|
||||
|
||||
async def _validate_subentry_from_config_entry(
|
||||
handler: SchemaCommonFlowHandler, user_input: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
# Standard behavior is to merge the result with the options.
|
||||
# In this case, we want to add a subentry so we update the options directly.
|
||||
observations: list[dict[str, Any]] = handler.options.setdefault(
|
||||
CONF_OBSERVATIONS, []
|
||||
)
|
||||
|
||||
if handler.parent_handler.cur_step is not None:
|
||||
user_input[CONF_PLATFORM] = handler.parent_handler.cur_step["step_id"]
|
||||
user_input = _validate_observation_subentry(
|
||||
user_input[CONF_PLATFORM],
|
||||
user_input,
|
||||
other_subentries=handler.options[CONF_OBSERVATIONS],
|
||||
)
|
||||
observations.append(user_input)
|
||||
return {}
|
||||
|
||||
|
||||
async def _get_description_placeholders(
|
||||
handler: SchemaCommonFlowHandler,
|
||||
) -> dict[str, str]:
|
||||
# Current step is None when were are about to start the first step
|
||||
if handler.parent_handler.cur_step is None:
|
||||
return {"url": "https://www.home-assistant.io/integrations/bayesian/"}
|
||||
return {
|
||||
"parent_sensor_name": handler.options[CONF_NAME],
|
||||
"device_class_on": translation.async_translate_state(
|
||||
handler.parent_handler.hass,
|
||||
"on",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
platform=None,
|
||||
translation_key=None,
|
||||
device_class=handler.options.get(CONF_DEVICE_CLASS, None),
|
||||
),
|
||||
"device_class_off": translation.async_translate_state(
|
||||
handler.parent_handler.hass,
|
||||
"off",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
platform=None,
|
||||
translation_key=None,
|
||||
device_class=handler.options.get(CONF_DEVICE_CLASS, None),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def _get_observation_menu_options(handler: SchemaCommonFlowHandler) -> list[str]:
|
||||
"""Return the menu options for the observation selector."""
|
||||
options = [typ.value for typ in ObservationTypes]
|
||||
if handler.options.get(CONF_OBSERVATIONS):
|
||||
options.append("finish")
|
||||
return options
|
||||
|
||||
|
||||
CONFIG_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
|
||||
str(USER): SchemaFlowFormStep(
|
||||
CONFIG_SCHEMA,
|
||||
validate_user_input=_validate_user,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(OBSERVATION_SELECTOR): SchemaFlowMenuStep(
|
||||
_get_observation_menu_options,
|
||||
),
|
||||
str(ObservationTypes.STATE): SchemaFlowFormStep(
|
||||
STATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
# Prevent the name of the bayesian sensor from being used as the suggested
|
||||
# name of the observations
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(ObservationTypes.NUMERIC_STATE): SchemaFlowFormStep(
|
||||
NUMERIC_STATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
str(ObservationTypes.TEMPLATE): SchemaFlowFormStep(
|
||||
TEMPLATE_SUBSCHEMA,
|
||||
next_step=str(OBSERVATION_SELECTOR),
|
||||
validate_user_input=_validate_subentry_from_config_entry,
|
||||
suggested_values=None,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
"finish": SchemaFlowFormStep(),
|
||||
}
|
||||
|
||||
|
||||
OPTIONS_FLOW: dict[str, SchemaFlowMenuStep | SchemaFlowFormStep] = {
|
||||
str(OptionsFlowSteps.INIT): SchemaFlowFormStep(
|
||||
OPTIONS_SCHEMA,
|
||||
suggested_values=_get_base_suggested_values,
|
||||
validate_user_input=_validate_user,
|
||||
description_placeholders=_get_description_placeholders,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class BayesianConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN):
|
||||
"""Bayesian config flow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
config_flow = CONFIG_FLOW
|
||||
options_flow = OPTIONS_FLOW
|
||||
|
||||
@classmethod
|
||||
@callback
|
||||
def async_get_supported_subentry_types(
|
||||
cls, config_entry: ConfigEntry
|
||||
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||
"""Return subentries supported by this integration."""
|
||||
return {"observation": ObservationSubentryFlowHandler}
|
||||
|
||||
def async_config_entry_title(self, options: Mapping[str, str]) -> str:
|
||||
"""Return config entry title."""
|
||||
name: str = options[CONF_NAME]
|
||||
return name
|
||||
|
||||
@callback
|
||||
def async_create_entry(
|
||||
self,
|
||||
data: Mapping[str, Any],
|
||||
**kwargs: Any,
|
||||
) -> ConfigFlowResult:
|
||||
"""Finish config flow and create a config entry."""
|
||||
data = dict(data)
|
||||
observations = data.pop(CONF_OBSERVATIONS)
|
||||
subentries: list[ConfigSubentryData] = [
|
||||
ConfigSubentryData(
|
||||
data=observation,
|
||||
title=observation[CONF_NAME],
|
||||
subentry_type="observation",
|
||||
unique_id=None,
|
||||
)
|
||||
for observation in observations
|
||||
]
|
||||
|
||||
self.async_config_flow_finished(data)
|
||||
return super().async_create_entry(data=data, subentries=subentries, **kwargs)
|
||||
|
||||
|
||||
class ObservationSubentryFlowHandler(ConfigSubentryFlow):
|
||||
"""Handle subentry flow for adding and modifying a topic."""
|
||||
|
||||
async def step_common(
|
||||
self,
|
||||
user_input: dict[str, Any] | None,
|
||||
obs_type: ObservationTypes,
|
||||
reconfiguring: bool = False,
|
||||
) -> SubentryFlowResult:
|
||||
"""Use common logic within the named steps."""
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
other_subentries = None
|
||||
if obs_type == str(ObservationTypes.NUMERIC_STATE):
|
||||
other_subentries = [
|
||||
dict(se.data) for se in self._get_entry().subentries.values()
|
||||
]
|
||||
# If we are reconfiguring a subentry we don't want to compare with self
|
||||
if reconfiguring:
|
||||
sub_entry = self._get_reconfigure_subentry()
|
||||
if other_subentries is not None:
|
||||
other_subentries.remove(dict(sub_entry.data))
|
||||
|
||||
if user_input is not None:
|
||||
try:
|
||||
user_input = _validate_observation_subentry(
|
||||
obs_type,
|
||||
user_input,
|
||||
other_subentries=other_subentries,
|
||||
)
|
||||
if reconfiguring:
|
||||
return self.async_update_and_abort(
|
||||
self._get_entry(),
|
||||
sub_entry,
|
||||
title=user_input.get(CONF_NAME, sub_entry.data[CONF_NAME]),
|
||||
data_updates=user_input,
|
||||
)
|
||||
return self.async_create_entry(
|
||||
title=user_input.get(CONF_NAME),
|
||||
data=user_input,
|
||||
)
|
||||
except SchemaFlowError as err:
|
||||
errors["base"] = str(err)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure" if reconfiguring else str(obs_type),
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=_select_observation_schema(obs_type),
|
||||
suggested_values=_get_observation_values_for_editing(sub_entry)
|
||||
if reconfiguring
|
||||
else None,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders={
|
||||
"parent_sensor_name": self._get_entry().title,
|
||||
"device_class_on": translation.async_translate_state(
|
||||
self.hass,
|
||||
"on",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
platform=None,
|
||||
translation_key=None,
|
||||
device_class=self._get_entry().options.get(CONF_DEVICE_CLASS, None),
|
||||
),
|
||||
"device_class_off": translation.async_translate_state(
|
||||
self.hass,
|
||||
"off",
|
||||
BINARY_SENSOR_DOMAIN,
|
||||
platform=None,
|
||||
translation_key=None,
|
||||
device_class=self._get_entry().options.get(CONF_DEVICE_CLASS, None),
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""User flow to add a new observation."""
|
||||
|
||||
return self.async_show_menu(
|
||||
step_id="user",
|
||||
menu_options=[typ.value for typ in ObservationTypes],
|
||||
)
|
||||
|
||||
async def async_step_state(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""User flow to add a state observation. Function name must be in the format async_step_{observation_type}."""
|
||||
|
||||
return await self.step_common(
|
||||
user_input=user_input, obs_type=ObservationTypes.STATE
|
||||
)
|
||||
|
||||
async def async_step_numeric_state(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""User flow to add a new numeric state observation, (a numeric range). Function name must be in the format async_step_{observation_type}."""
|
||||
|
||||
return await self.step_common(
|
||||
user_input=user_input, obs_type=ObservationTypes.NUMERIC_STATE
|
||||
)
|
||||
|
||||
async def async_step_template(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""User flow to add a new template observation. Function name must be in the format async_step_{observation_type}."""
|
||||
|
||||
return await self.step_common(
|
||||
user_input=user_input, obs_type=ObservationTypes.TEMPLATE
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> SubentryFlowResult:
|
||||
"""Enable the reconfigure button for observations. Function name must be async_step_reconfigure to be recognised by hass."""
|
||||
|
||||
sub_entry = self._get_reconfigure_subentry()
|
||||
|
||||
return await self.step_common(
|
||||
user_input=user_input,
|
||||
obs_type=ObservationTypes(sub_entry.data[CONF_PLATFORM]),
|
||||
reconfiguring=True,
|
||||
)
|
@@ -1,5 +1,9 @@
|
||||
"""Consts for using in modules."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "bayesian"
|
||||
PLATFORMS = [Platform.BINARY_SENSOR]
|
||||
ATTR_OBSERVATIONS = "observations"
|
||||
ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities"
|
||||
ATTR_PROBABILITY = "probability"
|
||||
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
|
||||
from . import DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .helpers import Observation
|
||||
|
||||
|
||||
|
@@ -2,8 +2,9 @@
|
||||
"domain": "bayesian",
|
||||
"name": "Bayesian",
|
||||
"codeowners": ["@HarvsG"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/bayesian",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"integration_type": "service",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
@@ -14,5 +14,264 @@
|
||||
"name": "[%key:common::action::reload%]",
|
||||
"description": "Reloads Bayesian sensors from the YAML-configuration."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"extreme_prior_error": "[%key:component::bayesian::config::error::extreme_prior_error%]",
|
||||
"extreme_threshold_error": "[%key:component::bayesian::config::error::extreme_threshold_error%]",
|
||||
"equal_probabilities": "[%key:component::bayesian::config::error::equal_probabilities%]",
|
||||
"extreme_prob_given_error": "[%key:component::bayesian::config::error::extreme_prob_given_error%]"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Sensor options",
|
||||
"description": "These options affect how much evidence is required for the Bayesian sensor to be considered 'on'.",
|
||||
"data": {
|
||||
"probability_threshold": "[%key:component::bayesian::config::step::user::data::probability_threshold%]",
|
||||
"prior": "[%key:component::bayesian::config::step::user::data::prior%]",
|
||||
"device_class": "[%key:component::bayesian::config::step::user::data::device_class%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"probability_threshold": "[%key:component::bayesian::config::step::user::data_description::probability_threshold%]",
|
||||
"prior": "[%key:component::bayesian::config::step::user::data_description::prior%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"error": {
|
||||
"extreme_prior_error": "'Prior' set to 0% means that it is impossible for the sensor to show 'on' and 100% means it will never show 'off', use a close number like 0.1% or 99.9% instead",
|
||||
"extreme_threshold_error": "'Probability threshold' set to 0% means that the sensor will always be 'on' and 100% mean it will always be 'off', use a close number like 0.1% or 99.9% instead",
|
||||
"equal_probabilities": "If 'Probability given true' and 'Probability given false' are equal, this observation can have no effect, and is therefore redundant",
|
||||
"extreme_prob_given_error": "If either 'Probability given false' or 'Probability given true' is 0 or 100 this will create certainties that override all other observations, use numbers close to 0 or 100 instead",
|
||||
"above_below": "Invalid range: 'Above' must be less than 'Below' when both are set.",
|
||||
"above_or_below": "Invalid range: At least one of 'Above' or 'Below' must be set.",
|
||||
"overlapping_ranges": "Invalid range: The 'Above' and 'Below' values overlap with another observation for the same entity."
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add a Bayesian sensor",
|
||||
"description": "Create a binary sensor which observes the state of multiple sensors to estimate whether an event is occurring, or if something is true. See [the documentation]({url}) for more details.",
|
||||
"data": {
|
||||
"probability_threshold": "Probability threshold",
|
||||
"prior": "Prior",
|
||||
"device_class": "Device class",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
},
|
||||
"data_description": {
|
||||
"probability_threshold": "The probability above which the sensor will show as 'on'. 50% should produce the most accurate result. Use numbers greater than 50% if avoiding false positives is important, or vice-versa.",
|
||||
"prior": "The baseline probabilty the sensor should be 'on', this is usually the percentage of time it is true. For example, for a sensor 'Everyone sleeping' it might be 8 hours a day, 33%.",
|
||||
"device_class": "Choose the device class you would like the sensor to show as."
|
||||
}
|
||||
},
|
||||
"observation_selector": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::user::title%]",
|
||||
"description": "[%key:component::bayesian::config_subentries::observation::step::user::description%]",
|
||||
"menu_options": {
|
||||
"state": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::state%]",
|
||||
"numeric_state": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::numeric_state%]",
|
||||
"template": "[%key:component::bayesian::config_subentries::observation::step::user::menu_options::template%]",
|
||||
"finish": "Finish"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
|
||||
"description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]",
|
||||
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
|
||||
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data::to_state%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
|
||||
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::to_state%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
},
|
||||
"numeric_state": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
|
||||
"description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
|
||||
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::above%]",
|
||||
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::below%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
|
||||
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::above%]",
|
||||
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::below%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
|
||||
"description": "[%key:component::bayesian::config_subentries::observation::step::template::description%]",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data::value_template%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data_description::value_template%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"config_subentries": {
|
||||
"observation": {
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Add an observation",
|
||||
"description": "'Observations' are the sensor or template values that are monitored and then combined in order to inform the Bayesian sensor's final probability. Each observation will update the probability of the Bayesian sensor if it is detected, or if it is not detected. If the state of the entity becomes `unavailable` or `unknown` it will be ignored. If more than one state or more than one numeric range is configured for the same entity then inverse detections will be ignored.",
|
||||
"menu_options": {
|
||||
"state": "Add an observation for a sensor's state",
|
||||
"numeric_state": "Add an observation for a numeric range",
|
||||
"template": "Add an observation for a template"
|
||||
}
|
||||
},
|
||||
"state": {
|
||||
"title": "Add a Bayesian sensor",
|
||||
"description": "Add an observation which evaluates to `True` when the value of the sensor exactly matches *'To state'*. When `False`, it will update the prior with probabilities that are the inverse of those set below. This behaviour can be overridden by adding observations for the same entity's other states.",
|
||||
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "Entity",
|
||||
"to_state": "To state",
|
||||
"prob_given_true": "Probability when {parent_sensor_name} is {device_class_on}",
|
||||
"prob_given_false": "Probability when {parent_sensor_name} is {device_class_off}"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "This name will be used for to identify this observation for editing in the future.",
|
||||
"entity_id": "An entity that is correlated with `{parent_sensor_name}`.",
|
||||
"to_state": "The state of the sensor for which the observation will be considered `True`.",
|
||||
"prob_given_true": "The estimated probability or proportion of time this observation is `True` while `{parent_sensor_name}` is, or should be, `{device_class_on}`.",
|
||||
"prob_given_false": "The estimated probability or proportion of time this observation is `True` while `{parent_sensor_name}` is, or should be, `{device_class_off}`."
|
||||
}
|
||||
},
|
||||
"numeric_state": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
|
||||
"description": "Add an observation which evaluates to `True` when a numeric sensor is within a chosen range.",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
|
||||
"above": "Optional - the lower end of the numeric range. Values exactly matching this will not count",
|
||||
"below": "Optional - the upper end of the numeric range. Values exactly matching this will only count if more than one range is configured for the same entity.",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
},
|
||||
"template": {
|
||||
"title": "[%key:component::bayesian::config_subentries::observation::step::state::title%]",
|
||||
"description": "Add a custom observation which evaluates whether a template is observed (`True`) or not (`False`).",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"value_template": "Template",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"value_template": "A template that evaluates to `True` will update the prior accordingly, A template that returns `False` or `None` will update the prior with inverse probabilities. A template that returns an error will not update probabilities. Results are coerced into being `True` or `False`",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
},
|
||||
"reconfigure": {
|
||||
"title": "Edit observation",
|
||||
"description": "[%key:component::bayesian::config_subentries::observation::step::state::description%]",
|
||||
"data": {
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data::entity_id%]",
|
||||
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data::to_state%]",
|
||||
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::above%]",
|
||||
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data::below%]",
|
||||
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data::value_template%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data::prob_given_false%]"
|
||||
},
|
||||
"data_description": {
|
||||
"name": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::name%]",
|
||||
"entity_id": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::entity_id%]",
|
||||
"to_state": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::to_state%]",
|
||||
"above": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::above%]",
|
||||
"below": "[%key:component::bayesian::config_subentries::observation::step::numeric_state::data_description::below%]",
|
||||
"value_template": "[%key:component::bayesian::config_subentries::observation::step::template::data_description::value_template%]",
|
||||
"prob_given_true": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_true%]",
|
||||
"prob_given_false": "[%key:component::bayesian::config_subentries::observation::step::state::data_description::prob_given_false%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "[%key:component::bayesian::config_subentries::observation::step::user::title%]"
|
||||
},
|
||||
"entry_type": "Observation",
|
||||
"error": {
|
||||
"equal_probabilities": "[%key:component::bayesian::config::error::equal_probabilities%]",
|
||||
"extreme_prob_given_error": "[%key:component::bayesian::config::error::extreme_prob_given_error%]",
|
||||
"above_below": "[%key:component::bayesian::config::error::above_below%]",
|
||||
"above_or_below": "[%key:component::bayesian::config::error::above_or_below%]",
|
||||
"overlapping_ranges": "[%key:component::bayesian::config::error::overlapping_ranges%]"
|
||||
},
|
||||
"abort": {
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"binary_sensor_device_class": {
|
||||
"options": {
|
||||
"battery": "[%key:component::binary_sensor::entity_component::battery::name%]",
|
||||
"battery_charging": "[%key:component::binary_sensor::entity_component::battery_charging::name%]",
|
||||
"carbon_monoxide": "[%key:component::binary_sensor::entity_component::carbon_monoxide::name%]",
|
||||
"cold": "[%key:component::binary_sensor::entity_component::cold::name%]",
|
||||
"connectivity": "[%key:component::binary_sensor::entity_component::connectivity::name%]",
|
||||
"door": "[%key:component::binary_sensor::entity_component::door::name%]",
|
||||
"garage_door": "[%key:component::binary_sensor::entity_component::garage_door::name%]",
|
||||
"gas": "[%key:component::binary_sensor::entity_component::gas::name%]",
|
||||
"heat": "[%key:component::binary_sensor::entity_component::heat::name%]",
|
||||
"light": "[%key:component::binary_sensor::entity_component::light::name%]",
|
||||
"lock": "[%key:component::binary_sensor::entity_component::lock::name%]",
|
||||
"moisture": "[%key:component::binary_sensor::entity_component::moisture::name%]",
|
||||
"motion": "[%key:component::binary_sensor::entity_component::motion::name%]",
|
||||
"moving": "[%key:component::binary_sensor::entity_component::moving::name%]",
|
||||
"occupancy": "[%key:component::binary_sensor::entity_component::occupancy::name%]",
|
||||
"opening": "[%key:component::binary_sensor::entity_component::opening::name%]",
|
||||
"plug": "[%key:component::binary_sensor::entity_component::plug::name%]",
|
||||
"power": "[%key:component::binary_sensor::entity_component::power::name%]",
|
||||
"presence": "[%key:component::binary_sensor::entity_component::presence::name%]",
|
||||
"problem": "[%key:component::binary_sensor::entity_component::problem::name%]",
|
||||
"running": "[%key:component::binary_sensor::entity_component::running::name%]",
|
||||
"safety": "[%key:component::binary_sensor::entity_component::safety::name%]",
|
||||
"smoke": "[%key:component::binary_sensor::entity_component::smoke::name%]",
|
||||
"sound": "[%key:component::binary_sensor::entity_component::sound::name%]",
|
||||
"tamper": "[%key:component::binary_sensor::entity_component::tamper::name%]",
|
||||
"update": "[%key:component::binary_sensor::entity_component::update::name%]",
|
||||
"vibration": "[%key:component::binary_sensor::entity_component::vibration::name%]",
|
||||
"window": "[%key:component::binary_sensor::entity_component::window::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1
homeassistant/generated/config_flows.py
generated
1
homeassistant/generated/config_flows.py
generated
@@ -87,6 +87,7 @@ FLOWS = {
|
||||
"baf",
|
||||
"balboa",
|
||||
"bang_olufsen",
|
||||
"bayesian",
|
||||
"blebox",
|
||||
"blink",
|
||||
"blue_current",
|
||||
|
@@ -665,6 +665,12 @@
|
||||
"integration_type": "virtual",
|
||||
"supported_by": "whirlpool"
|
||||
},
|
||||
"bayesian": {
|
||||
"name": "Bayesian",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "calculated"
|
||||
},
|
||||
"bbox": {
|
||||
"name": "Bbox",
|
||||
"integration_type": "hub",
|
||||
@@ -7764,12 +7770,6 @@
|
||||
}
|
||||
},
|
||||
"helper": {
|
||||
"bayesian": {
|
||||
"name": "Bayesian",
|
||||
"integration_type": "helper",
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"counter": {
|
||||
"integration_type": "helper",
|
||||
"config_flow": false
|
||||
|
@@ -7,7 +7,8 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from homeassistant import config as hass_config
|
||||
from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian
|
||||
from homeassistant.components.bayesian import binary_sensor as bayesian
|
||||
from homeassistant.components.bayesian.const import DOMAIN
|
||||
from homeassistant.components.homeassistant import (
|
||||
DOMAIN as HA_DOMAIN,
|
||||
SERVICE_UPDATE_ENTITY,
|
||||
|
1211
tests/components/bayesian/test_config_flow.py
Normal file
1211
tests/components/bayesian/test_config_flow.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user