Fix Bayesian sensor to use negative observations (#67631)
Co-authored-by: Diogo Gomes <diogogomes@gmail.com>pull/78282/head
parent
faad904cbc
commit
49eeeae51d
|
@ -127,6 +127,8 @@ build.json @home-assistant/supervisor
|
|||
/tests/components/baf/ @bdraco @jfroy
|
||||
/homeassistant/components/balboa/ @garbled1
|
||||
/tests/components/balboa/ @garbled1
|
||||
/homeassistant/components/bayesian/ @HarvsG
|
||||
/tests/components/bayesian/ @HarvsG
|
||||
/homeassistant/components/beewi_smartclim/ @alemuro
|
||||
/homeassistant/components/binary_sensor/ @home-assistant/core
|
||||
/tests/components/binary_sensor/ @home-assistant/core
|
||||
|
|
|
@ -16,6 +16,7 @@ from homeassistant.const import (
|
|||
CONF_PLATFORM,
|
||||
CONF_STATE,
|
||||
CONF_VALUE_TEMPLATE,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
@ -60,7 +61,7 @@ NUMERIC_STATE_SCHEMA = vol.Schema(
|
|||
vol.Optional(CONF_ABOVE): vol.Coerce(float),
|
||||
vol.Optional(CONF_BELOW): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
},
|
||||
required=True,
|
||||
)
|
||||
|
@ -71,7 +72,7 @@ STATE_SCHEMA = vol.Schema(
|
|||
vol.Required(CONF_ENTITY_ID): cv.entity_id,
|
||||
vol.Required(CONF_TO_STATE): cv.string,
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
},
|
||||
required=True,
|
||||
)
|
||||
|
@ -81,7 +82,7 @@ TEMPLATE_SCHEMA = vol.Schema(
|
|||
CONF_PLATFORM: CONF_TEMPLATE,
|
||||
vol.Required(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_P_GIVEN_T): vol.Coerce(float),
|
||||
vol.Optional(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
vol.Required(CONF_P_GIVEN_F): vol.Coerce(float),
|
||||
},
|
||||
required=True,
|
||||
)
|
||||
|
@ -160,6 +161,7 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
self.observation_handlers = {
|
||||
"numeric_state": self._process_numeric_state,
|
||||
"state": self._process_state,
|
||||
"multi_state": self._process_multi_state,
|
||||
}
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
@ -185,10 +187,6 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
When a state changes, we must update our list of current observations,
|
||||
then calculate the new probability.
|
||||
"""
|
||||
new_state = event.data.get("new_state")
|
||||
|
||||
if new_state is None or new_state.state == STATE_UNKNOWN:
|
||||
return
|
||||
|
||||
entity = event.data.get("entity_id")
|
||||
|
||||
|
@ -210,7 +208,6 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
template = track_template_result.template
|
||||
result = track_template_result.result
|
||||
entity = event and event.data.get("entity_id")
|
||||
|
||||
if isinstance(result, TemplateError):
|
||||
_LOGGER.error(
|
||||
"TemplateError('%s') "
|
||||
|
@ -221,15 +218,12 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
self.entity_id,
|
||||
)
|
||||
|
||||
should_trigger = False
|
||||
observation = None
|
||||
else:
|
||||
should_trigger = result_as_boolean(result)
|
||||
observation = result_as_boolean(result)
|
||||
|
||||
for obs in self.observations_by_template[template]:
|
||||
if should_trigger:
|
||||
obs_entry = {"entity_id": entity, **obs}
|
||||
else:
|
||||
obs_entry = None
|
||||
obs_entry = {"entity_id": entity, "observation": observation, **obs}
|
||||
self.current_observations[obs["id"]] = obs_entry
|
||||
|
||||
if event:
|
||||
|
@ -259,6 +253,7 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
|
||||
def _initialize_current_observations(self):
|
||||
local_observations = OrderedDict({})
|
||||
|
||||
for entity in self.observations_by_entity:
|
||||
local_observations.update(self._record_entity_observations(entity))
|
||||
return local_observations
|
||||
|
@ -269,13 +264,13 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
for entity_obs in self.observations_by_entity[entity]:
|
||||
platform = entity_obs["platform"]
|
||||
|
||||
should_trigger = self.observation_handlers[platform](entity_obs)
|
||||
|
||||
if should_trigger:
|
||||
obs_entry = {"entity_id": entity, **entity_obs}
|
||||
else:
|
||||
obs_entry = None
|
||||
observation = self.observation_handlers[platform](entity_obs)
|
||||
|
||||
obs_entry = {
|
||||
"entity_id": entity,
|
||||
"observation": observation,
|
||||
**entity_obs,
|
||||
}
|
||||
local_observations[entity_obs["id"]] = obs_entry
|
||||
|
||||
return local_observations
|
||||
|
@ -285,11 +280,28 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
|
||||
for obs in self.current_observations.values():
|
||||
if obs is not None:
|
||||
prior = update_probability(
|
||||
prior,
|
||||
obs["prob_given_true"],
|
||||
obs.get("prob_given_false", 1 - obs["prob_given_true"]),
|
||||
)
|
||||
if obs["observation"] is True:
|
||||
prior = update_probability(
|
||||
prior,
|
||||
obs["prob_given_true"],
|
||||
obs["prob_given_false"],
|
||||
)
|
||||
elif obs["observation"] is False:
|
||||
prior = update_probability(
|
||||
prior,
|
||||
1 - obs["prob_given_true"],
|
||||
1 - obs["prob_given_false"],
|
||||
)
|
||||
elif obs["observation"] is None:
|
||||
if obs["entity_id"] is not None:
|
||||
_LOGGER.debug(
|
||||
"Observation for entity '%s' returned None, it will not be used for Bayesian updating",
|
||||
obs["entity_id"],
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Observation for template entity returned None rather than a valid boolean, it will not be used for Bayesian updating",
|
||||
)
|
||||
|
||||
return prior
|
||||
|
||||
|
@ -307,17 +319,21 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
for all relevant observations to be looked up via their `entity_id`.
|
||||
"""
|
||||
|
||||
observations_by_entity = {}
|
||||
for ind, obs in enumerate(self._observations):
|
||||
obs["id"] = ind
|
||||
observations_by_entity: dict[str, list[OrderedDict]] = {}
|
||||
for i, obs in enumerate(self._observations):
|
||||
obs["id"] = i
|
||||
|
||||
if "entity_id" not in obs:
|
||||
continue
|
||||
observations_by_entity.setdefault(obs["entity_id"], []).append(obs)
|
||||
|
||||
entity_ids = [obs["entity_id"]]
|
||||
|
||||
for e_id in entity_ids:
|
||||
observations_by_entity.setdefault(e_id, []).append(obs)
|
||||
for li_of_dicts in observations_by_entity.values():
|
||||
if len(li_of_dicts) == 1:
|
||||
continue
|
||||
for ord_dict in li_of_dicts:
|
||||
if ord_dict["platform"] != "state":
|
||||
continue
|
||||
ord_dict["platform"] = "multi_state"
|
||||
|
||||
return observations_by_entity
|
||||
|
||||
|
@ -348,10 +364,12 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
return observations_by_template
|
||||
|
||||
def _process_numeric_state(self, entity_observation):
|
||||
"""Return True if numeric condition is met."""
|
||||
"""Return True if numeric condition is met, return False if not, return None otherwise."""
|
||||
entity = entity_observation["entity_id"]
|
||||
|
||||
try:
|
||||
if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]):
|
||||
return None
|
||||
return condition.async_numeric_state(
|
||||
self.hass,
|
||||
entity,
|
||||
|
@ -361,18 +379,31 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
entity_observation,
|
||||
)
|
||||
except ConditionError:
|
||||
return False
|
||||
return None
|
||||
|
||||
def _process_state(self, entity_observation):
|
||||
"""Return True if state conditions are met."""
|
||||
entity = entity_observation["entity_id"]
|
||||
|
||||
try:
|
||||
if condition.state(self.hass, entity, [STATE_UNKNOWN, STATE_UNAVAILABLE]):
|
||||
return None
|
||||
|
||||
return condition.state(
|
||||
self.hass, entity, entity_observation.get("to_state")
|
||||
)
|
||||
except ConditionError:
|
||||
return False
|
||||
return None
|
||||
|
||||
def _process_multi_state(self, entity_observation):
|
||||
"""Return True if state conditions are met."""
|
||||
entity = entity_observation["entity_id"]
|
||||
|
||||
try:
|
||||
if condition.state(self.hass, entity, entity_observation.get("to_state")):
|
||||
return True
|
||||
except ConditionError:
|
||||
return None
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self):
|
||||
|
@ -390,7 +421,9 @@ class BayesianBinarySensor(BinarySensorEntity):
|
|||
{
|
||||
obs.get("entity_id")
|
||||
for obs in self.current_observations.values()
|
||||
if obs is not None and obs.get("entity_id") is not None
|
||||
if obs is not None
|
||||
and obs.get("entity_id") is not None
|
||||
and obs.get("observation") is not None
|
||||
}
|
||||
),
|
||||
ATTR_PROBABILITY: round(self.probability, 2),
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"domain": "bayesian",
|
||||
"name": "Bayesian",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bayesian",
|
||||
"codeowners": [],
|
||||
"codeowners": ["@HarvsG"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_polling"
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.const import (
|
|||
SERVICE_RELOAD,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import Context, callback
|
||||
|
@ -56,7 +57,7 @@ async def test_load_values_when_added_to_hass(hass):
|
|||
|
||||
async def test_unknown_state_does_not_influence_probability(hass):
|
||||
"""Test that an unknown state does not change the output probability."""
|
||||
|
||||
prior = 0.2
|
||||
config = {
|
||||
"binary_sensor": {
|
||||
"name": "Test_Binary",
|
||||
|
@ -70,11 +71,12 @@ async def test_unknown_state_does_not_influence_probability(hass):
|
|||
"prob_given_false": 0.4,
|
||||
}
|
||||
],
|
||||
"prior": 0.2,
|
||||
"prior": prior,
|
||||
"probability_threshold": 0.32,
|
||||
}
|
||||
}
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
@ -82,7 +84,8 @@ async def test_unknown_state_does_not_influence_probability(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("observations") == []
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert state.attributes.get("probability") == prior
|
||||
|
||||
|
||||
async def test_sensor_numeric_state(hass):
|
||||
|
@ -97,7 +100,8 @@ async def test_sensor_numeric_state(hass):
|
|||
"entity_id": "sensor.test_monitored",
|
||||
"below": 10,
|
||||
"above": 5,
|
||||
"prob_given_true": 0.6,
|
||||
"prob_given_true": 0.7,
|
||||
"prob_given_false": 0.4,
|
||||
},
|
||||
{
|
||||
"platform": "numeric_state",
|
||||
|
@ -105,7 +109,7 @@ async def test_sensor_numeric_state(hass):
|
|||
"below": 7,
|
||||
"above": 5,
|
||||
"prob_given_true": 0.9,
|
||||
"prob_given_false": 0.1,
|
||||
"prob_given_false": 0.2,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -115,40 +119,61 @@ async def test_sensor_numeric_state(hass):
|
|||
assert await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 6)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert abs(state.attributes.get("probability") - 0.304) < 0.01
|
||||
# A = sensor.test_binary being ON
|
||||
# B = sensor.test_monitored in the range [5, 10]
|
||||
# Bayes theorum is P(A|B) = P(B|A) * P(A) / P(B|A)*P(A) + P(B|~A)*P(~A).
|
||||
# Where P(B|A) is prob_given_true and P(B|~A) is prob_given_false
|
||||
# Calculated using P(A) = 0.2, P(B|A) = 0.7, P(B|~A) = 0.4 -> 0.30
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 4)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert [] == state.attributes.get("observations")
|
||||
assert state.attributes.get("probability") == 0.2
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert abs(state.attributes.get("probability") - 0.111) < 0.01
|
||||
# As abve but since the value is equal to 4 then this is a negative observation (~B) where P(~B) == 1 - P(B) because B is binary
|
||||
# We therefore want to calculate P(A|~B) so we use P(~B|A) (1-0.7) and P(~B|~A) (1-0.4)
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 1-0.7 (as negative observation), P(~B|notA) = 1-0.4 -> 0.11
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 6)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", 4)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", 6)
|
||||
hass.states.async_set("sensor.test_monitored1", 6)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.6
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.7
|
||||
assert state.attributes.get("observations")[1]["prob_given_true"] == 0.9
|
||||
assert state.attributes.get("observations")[1]["prob_given_false"] == 0.1
|
||||
assert round(abs(0.77 - state.attributes.get("probability")), 7) == 0
|
||||
assert state.attributes.get("observations")[1]["prob_given_false"] == 0.2
|
||||
assert abs(state.attributes.get("probability") - 0.663) < 0.01
|
||||
# Here we have two positive observations as both are in range. We do a 2-step bayes. The output of the first is used as the (updated) prior in the second.
|
||||
# 1st step P(A) = 0.2, P(B|A) = 0.7, P(B|notA) = 0.4 -> 0.304
|
||||
# 2nd update: P(A) = 0.304, P(B|A) = 0.9, P(B|notA) = 0.2 -> 0.663
|
||||
|
||||
assert state.state == "on"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 6)
|
||||
hass.states.async_set("sensor.test_monitored1", 0)
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", 4)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("probability") == 0.2
|
||||
assert abs(state.attributes.get("probability") - 0.0153) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.3, P(~B|notA) = 0.6 -> 0.11
|
||||
# 2nd update: P(A) = 0.111, P(~B|A) = 0.1, P(~B|notA) = 0.8
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
|
@ -162,6 +187,7 @@ async def test_sensor_numeric_state(hass):
|
|||
|
||||
async def test_sensor_state(hass):
|
||||
"""Test sensor on state platform observations."""
|
||||
prior = 0.2
|
||||
config = {
|
||||
"binary_sensor": {
|
||||
"name": "Test_Binary",
|
||||
|
@ -175,7 +201,7 @@ async def test_sensor_state(hass):
|
|||
"prob_given_false": 0.4,
|
||||
}
|
||||
],
|
||||
"prior": 0.2,
|
||||
"prior": prior,
|
||||
"probability_threshold": 0.32,
|
||||
}
|
||||
}
|
||||
|
@ -184,36 +210,51 @@ async def test_sensor_state(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert [] == state.attributes.get("observations")
|
||||
assert state.attributes.get("probability") == 0.2
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8
|
||||
assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4
|
||||
assert abs(0.0769 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8
|
||||
assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4
|
||||
assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert abs(0.33 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.8 (as negative observation), P(~B|notA) = 0.4
|
||||
assert state.state == "on"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
hass.states.async_remove("sensor.test_monitored")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert round(abs(0.2 - state.attributes.get("probability")), 7) == 0
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert abs(prior - state.attributes.get("probability")) < 0.01
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert abs(prior - state.attributes.get("probability")) < 0.01
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert abs(prior - state.attributes.get("probability")) < 0.01
|
||||
assert state.state == "off"
|
||||
|
||||
|
||||
|
@ -243,32 +284,29 @@ async def test_sensor_value_template(hass):
|
|||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert [] == state.attributes.get("observations")
|
||||
assert state.attributes.get("probability") == 0.2
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert abs(0.0769 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8
|
||||
assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4
|
||||
assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0
|
||||
assert abs(0.33333 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.8, P(B|notA) = 0.4
|
||||
|
||||
assert state.state == "on"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "on")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert round(abs(0.2 - state.attributes.get("probability")), 7) == 0
|
||||
assert abs(0.076923 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(~B|A) = 0.2 (as negative observation), P(~B|notA) = 0.6
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
|
@ -285,6 +323,7 @@ async def test_threshold(hass):
|
|||
"entity_id": "sensor.test_monitored",
|
||||
"to_state": "on",
|
||||
"prob_given_true": 1.0,
|
||||
"prob_given_false": 0.0,
|
||||
}
|
||||
],
|
||||
"prior": 0.5,
|
||||
|
@ -305,7 +344,14 @@ async def test_threshold(hass):
|
|||
|
||||
|
||||
async def test_multiple_observations(hass):
|
||||
"""Test sensor with multiple observations of same entity."""
|
||||
"""
|
||||
Test sensor with multiple observations of same entity.
|
||||
|
||||
these entries should be labelled as 'multi_state' and negative observations ignored - as the outcome is not known to be binary.
|
||||
Before the merge of #67631 this practice was a common work-around for bayesian's ignoring of negative observations,
|
||||
this also preserves that function
|
||||
"""
|
||||
|
||||
config = {
|
||||
"binary_sensor": {
|
||||
"name": "Test_Binary",
|
||||
|
@ -323,7 +369,7 @@ async def test_multiple_observations(hass):
|
|||
"entity_id": "sensor.test_monitored",
|
||||
"to_state": "red",
|
||||
"prob_given_true": 0.2,
|
||||
"prob_given_false": 0.4,
|
||||
"prob_given_false": 0.6,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -335,40 +381,118 @@ async def test_multiple_observations(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
for key, attrs in state.attributes.items():
|
||||
for _, attrs in state.attributes.items():
|
||||
json.dumps(attrs)
|
||||
assert [] == state.attributes.get("observations")
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert state.attributes.get("probability") == 0.2
|
||||
# probability should be the same as the prior as negative observations are ignored in multi-state
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "blue")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "blue")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8
|
||||
assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4
|
||||
assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.8, P(B|notA) = 0.4
|
||||
|
||||
assert state.state == "on"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "blue")
|
||||
await hass.async_block_till_done()
|
||||
hass.states.async_set("sensor.test_monitored", "red")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert round(abs(0.11 - state.attributes.get("probability")), 7) == 0
|
||||
assert abs(0.076923 - state.attributes.get("probability")) < 0.01
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.2, P(B|notA) = 0.6
|
||||
|
||||
assert state.state == "off"
|
||||
assert state.attributes.get("observations")[0]["platform"] == "multi_state"
|
||||
assert state.attributes.get("observations")[1]["platform"] == "multi_state"
|
||||
|
||||
|
||||
async def test_multiple_numeric_observations(hass):
|
||||
"""Test sensor with multiple numeric observations of same entity."""
|
||||
|
||||
config = {
|
||||
"binary_sensor": {
|
||||
"platform": "bayesian",
|
||||
"name": "Test_Binary",
|
||||
"observations": [
|
||||
{
|
||||
"platform": "numeric_state",
|
||||
"entity_id": "sensor.test_monitored",
|
||||
"below": 10,
|
||||
"above": 0,
|
||||
"prob_given_true": 0.4,
|
||||
"prob_given_false": 0.0001,
|
||||
},
|
||||
{
|
||||
"platform": "numeric_state",
|
||||
"entity_id": "sensor.test_monitored",
|
||||
"below": 100,
|
||||
"above": 30,
|
||||
"prob_given_true": 0.6,
|
||||
"prob_given_false": 0.0001,
|
||||
},
|
||||
],
|
||||
"prior": 0.1,
|
||||
}
|
||||
}
|
||||
|
||||
assert await async_setup_component(hass, "binary_sensor", config)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", STATE_UNKNOWN)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
for _, attrs in state.attributes.items():
|
||||
json.dumps(attrs)
|
||||
assert state.attributes.get("occurred_observation_entities") == []
|
||||
assert state.attributes.get("probability") == 0.1
|
||||
# No observations made so probability should be the prior
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 20)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert round(abs(0.026 - state.attributes.get("probability")), 7) < 0.01
|
||||
# Step 1 Calculated where P(A) = 0.1, P(~B|A) = 0.6 (negative obs), P(~B|notA) = 0.9999 -> 0.0625
|
||||
# Step 2 P(A) = 0.0625, P(B|A) = 0.4 (negative obs), P(B|notA) = 0.9999 -> 0.26
|
||||
|
||||
assert state.state == "off"
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", 35)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
assert abs(1 - state.attributes.get("probability")) < 0.01
|
||||
# Step 1 Calculated where P(A) = 0.1, P(~B|A) = 0.6 (negative obs), P(~B|notA) = 0.9999 -> 0.0625
|
||||
# Step 2 P(A) = 0.0625, P(B|A) = 0.6, P(B|notA) = 0.0001 -> 0.9975
|
||||
|
||||
assert state.state == "on"
|
||||
assert state.attributes.get("observations")[0]["platform"] == "numeric_state"
|
||||
assert state.attributes.get("observations")[1]["platform"] == "numeric_state"
|
||||
|
||||
|
||||
async def test_probability_updates(hass):
|
||||
|
@ -377,8 +501,8 @@ async def test_probability_updates(hass):
|
|||
prob_given_false = [0.7, 0.4, 0.2]
|
||||
prior = 0.5
|
||||
|
||||
for pt, pf in zip(prob_given_true, prob_given_false):
|
||||
prior = bayesian.update_probability(prior, pt, pf)
|
||||
for p_t, p_f in zip(prob_given_true, prob_given_false):
|
||||
prior = bayesian.update_probability(prior, p_t, p_f)
|
||||
|
||||
assert round(abs(0.720000 - prior), 7) == 0
|
||||
|
||||
|
@ -386,8 +510,8 @@ async def test_probability_updates(hass):
|
|||
prob_given_false = [0.6, 0.4, 0.2]
|
||||
prior = 0.7
|
||||
|
||||
for pt, pf in zip(prob_given_true, prob_given_false):
|
||||
prior = bayesian.update_probability(prior, pt, pf)
|
||||
for p_t, p_f in zip(prob_given_true, prob_given_false):
|
||||
prior = bayesian.update_probability(prior, p_t, p_f)
|
||||
|
||||
assert round(abs(0.9130434782608695 - prior), 7) == 0
|
||||
|
||||
|
@ -410,6 +534,7 @@ async def test_observed_entities(hass):
|
|||
"platform": "template",
|
||||
"value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}",
|
||||
"prob_given_true": 0.9,
|
||||
"prob_given_false": 0.1,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -426,7 +551,9 @@ async def test_observed_entities(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert [] == state.attributes.get("occurred_observation_entities")
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
|
@ -463,6 +590,7 @@ async def test_state_attributes_are_serializable(hass):
|
|||
"platform": "template",
|
||||
"value_template": "{{is_state('sensor.test_monitored1','on') and is_state('sensor.test_monitored','off')}}",
|
||||
"prob_given_true": 0.9,
|
||||
"prob_given_false": 0.1,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -479,15 +607,17 @@ async def test_state_attributes_are_serializable(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert [] == state.attributes.get("occurred_observation_entities")
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
|
||||
hass.states.async_set("sensor.test_monitored", "off")
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get("binary_sensor.test_binary")
|
||||
assert ["sensor.test_monitored"] == state.attributes.get(
|
||||
"occurred_observation_entities"
|
||||
)
|
||||
assert state.attributes.get("occurred_observation_entities") == [
|
||||
"sensor.test_monitored"
|
||||
]
|
||||
|
||||
hass.states.async_set("sensor.test_monitored1", "on")
|
||||
await hass.async_block_till_done()
|
||||
|
@ -497,7 +627,7 @@ async def test_state_attributes_are_serializable(hass):
|
|||
state.attributes.get("occurred_observation_entities")
|
||||
)
|
||||
|
||||
for key, attrs in state.attributes.items():
|
||||
for _, attrs in state.attributes.items():
|
||||
json.dumps(attrs)
|
||||
|
||||
|
||||
|
@ -512,6 +642,7 @@ async def test_template_error(hass, caplog):
|
|||
"platform": "template",
|
||||
"value_template": "{{ xyz + 1 }}",
|
||||
"prob_given_true": 0.9,
|
||||
"prob_given_false": 0.1,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -633,11 +764,16 @@ async def test_monitored_sensor_goes_away(hass):
|
|||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get("binary_sensor.test_binary").state == "on"
|
||||
# Calculated using bayes theorum where P(A) = 0.2, P(B|A) = 0.9, P(B|notA) = 0.4 -> 0.36 (>0.32)
|
||||
|
||||
hass.states.async_remove("sensor.test_monitored")
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert hass.states.get("binary_sensor.test_binary").state == "on"
|
||||
assert (
|
||||
hass.states.get("binary_sensor.test_binary").attributes.get("probability")
|
||||
== 0.2
|
||||
)
|
||||
assert hass.states.get("binary_sensor.test_binary").state == "off"
|
||||
|
||||
|
||||
async def test_reload(hass):
|
||||
|
@ -696,7 +832,8 @@ async def test_template_triggers(hass):
|
|||
{
|
||||
"platform": "template",
|
||||
"value_template": "{{ states.input_boolean.test.state }}",
|
||||
"prob_given_true": 1999.9,
|
||||
"prob_given_true": 1.0,
|
||||
"prob_given_false": 0.0,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
@ -735,8 +872,8 @@ async def test_state_triggers(hass):
|
|||
"platform": "state",
|
||||
"entity_id": "sensor.test_monitored",
|
||||
"to_state": "off",
|
||||
"prob_given_true": 999.9,
|
||||
"prob_given_false": 999.4,
|
||||
"prob_given_true": 0.9999,
|
||||
"prob_given_false": 0.9994,
|
||||
},
|
||||
],
|
||||
"prior": 0.2,
|
||||
|
|
Loading…
Reference in New Issue