Fix Bayesian sensor to use negative observations (#67631)

Co-authored-by: Diogo Gomes <diogogomes@gmail.com>
This commit is contained in:
HarvsG 2022-09-26 03:02:35 +01:00 committed by GitHub
parent faad904cbc
commit 49eeeae51d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 278 additions and 106 deletions

View File

@ -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

View File

@ -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),

View File

@ -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"
}

View File

@ -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,