From 768b83139fe9704c35017f7dd8f9322cb38bc0c5 Mon Sep 17 00:00:00 2001 From: HarvsG <11440490+HarvsG@users.noreply.github.com> Date: Thu, 29 Sep 2022 01:39:15 +0000 Subject: [PATCH] Add to issue registry if user has mirrored entries for breaking in #67631 (#79208) Co-authored-by: Diogo Gomes --- .../components/bayesian/binary_sensor.py | 17 ++++ homeassistant/components/bayesian/repairs.py | 39 ++++++++ .../components/bayesian/translations/en.json | 8 ++ .../components/bayesian/test_binary_sensor.py | 92 +++++++++++++++++++ 4 files changed, 156 insertions(+) create mode 100644 homeassistant/components/bayesian/repairs.py create mode 100644 homeassistant/components/bayesian/translations/en.json diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index 73ebcc8b37e..0e943b2d0ad 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -34,6 +34,7 @@ from homeassistant.helpers.template import result_as_boolean from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from . import DOMAIN, PLATFORMS +from .repairs import raise_mirrored_entries ATTR_OBSERVATIONS = "observations" ATTR_OCCURRED_OBSERVATION_ENTITIES = "occurred_observation_entities" @@ -245,6 +246,22 @@ class BayesianBinarySensor(BinarySensorEntity): self.probability = self._calculate_new_probability() self._attr_is_on = bool(self.probability >= self._probability_threshold) + # detect mirrored entries + for entity, observations in self.observations_by_entity.items(): + raise_mirrored_entries( + self.hass, observations, text=f"{self._attr_name}/{entity}" + ) + + all_template_observations = [] + for value in self.observations_by_template.values(): + all_template_observations.append(value[0]) + if len(all_template_observations) == 2: + raise_mirrored_entries( + self.hass, + all_template_observations, + text=f"{self._attr_name}/{all_template_observations[0]['value_template']}", + ) + @callback def _recalculate_and_write_state(self): self.probability = self._calculate_new_probability() diff --git a/homeassistant/components/bayesian/repairs.py b/homeassistant/components/bayesian/repairs.py new file mode 100644 index 00000000000..a1391f8c550 --- /dev/null +++ b/homeassistant/components/bayesian/repairs.py @@ -0,0 +1,39 @@ +"""Helpers for generating repairs.""" +from __future__ import annotations + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import issue_registry + +from . import DOMAIN + + +def raise_mirrored_entries(hass: HomeAssistant, observations, text: str = "") -> None: + """If there are mirrored entries, the user is probably using a workaround for a patched bug.""" + if len(observations) != 2: + return + true_sums_1: bool = ( + round( + observations[0]["prob_given_true"] + observations[1]["prob_given_true"], 1 + ) + == 1.0 + ) + false_sums_1: bool = ( + round( + observations[0]["prob_given_false"] + observations[1]["prob_given_false"], 1 + ) + == 1.0 + ) + same_states: bool = observations[0]["platform"] == observations[1]["platform"] + if true_sums_1 & false_sums_1 & same_states: + issue_registry.async_create_issue( + hass, + DOMAIN, + "mirrored_entry/" + text, + breaks_in_ha_version="2022.10.0", + is_fixable=False, + is_persistent=False, + severity=issue_registry.IssueSeverity.WARNING, + translation_key="manual_migration", + translation_placeholders={"entity": text}, + learn_more_url="https://github.com/home-assistant/core/pull/67631", + ) diff --git a/homeassistant/components/bayesian/translations/en.json b/homeassistant/components/bayesian/translations/en.json new file mode 100644 index 00000000000..ae9e5645f73 --- /dev/null +++ b/homeassistant/components/bayesian/translations/en.json @@ -0,0 +1,8 @@ +{ + "issues": { + "manual_migration": { + "description": "The Bayesian integration now also updates the probability if the observed `to_state`, `above`, `below`, or `value_template` evaluates to `False` rather than only `True`. So it is no longer required to have duplicate, complementary entries for each binary state. Please remove the mirrored entry for `{entity}`.", + "title": "Manual YAML fix required for Bayesian" + } + } +} diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 357cacb4214..0344e2b9445 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -18,6 +18,7 @@ from homeassistant.const import ( ) from homeassistant.core import Context, callback from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.issue_registry import async_get from homeassistant.setup import async_setup_component from tests.common import get_fixture_path @@ -184,6 +185,8 @@ async def test_sensor_numeric_state(hass): assert state.state == "off" + assert len(async_get(hass).issues) == 0 + async def test_sensor_state(hass): """Test sensor on state platform observations.""" @@ -341,6 +344,7 @@ async def test_threshold(hass): assert round(abs(1.0 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" + assert len(async_get(hass).issues) == 0 async def test_multiple_observations(hass): @@ -495,6 +499,94 @@ async def test_multiple_numeric_observations(hass): assert state.attributes.get("observations")[1]["platform"] == "numeric_state" +async def test_mirrored_observations(hass): + """Test whether mirrored entries are detected and appropriate issues are created.""" + + config = { + "binary_sensor": { + "platform": "bayesian", + "name": "Test_Binary", + "observations": [ + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "on", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + }, + { + "platform": "state", + "entity_id": "binary_sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.2, + "prob_given_false": 0.59, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "above": 5, + "prob_given_true": 0.7, + "prob_given_false": 0.4, + }, + { + "platform": "numeric_state", + "entity_id": "sensor.test_monitored1", + "below": 5, + "prob_given_true": 0.3, + "prob_given_false": 0.6, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'off'}}", + "prob_given_true": 0.79, + "prob_given_false": 0.4, + }, + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored2') == 'on'}}", + "prob_given_true": 0.2, + "prob_given_false": 0.6, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "blue", + "prob_given_true": 0.33, + "prob_given_false": 0.8, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "green", + "prob_given_true": 0.3, + "prob_given_false": 0.15, + }, + { + "platform": "state", + "entity_id": "sensor.colour", + "to_state": "red", + "prob_given_true": 0.4, + "prob_given_false": 0.05, + }, + ], + "prior": 0.1, + } + } + assert len(async_get(hass).issues) == 0 + assert await async_setup_component(hass, "binary_sensor", config) + await hass.async_block_till_done() + hass.states.async_set("sensor.test_monitored2", "on") + await hass.async_block_till_done() + + assert len(async_get(hass).issues) == 3 + assert ( + async_get(hass).issues[ + ("bayesian", "mirrored_entry/Test_Binary/sensor.test_monitored1") + ] + is not None + ) + + async def test_probability_updates(hass): """Test probability update function.""" prob_given_true = [0.3, 0.6, 0.8]