From 3e9974324480fe113003b0c80a5be267d017691c Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 3 Oct 2019 06:14:35 +0200 Subject: [PATCH] Add device trigger support to sensor entities (#27133) * Add device trigger support to sensor entities * Fix typing * Fix tests, add test helper for comparing lists --- .../components/automation/numeric_state.py | 6 +- .../binary_sensor/device_trigger.py | 6 +- .../device_automation/toggle_entity.py | 4 +- .../components/sensor/device_trigger.py | 145 +++++++ homeassistant/components/sensor/strings.json | 26 ++ tests/common.py | 83 ++++ .../components/deconz/test_device_trigger.py | 11 +- .../components/sensor/test_device_trigger.py | 368 ++++++++++++++++++ tests/conftest.py | 7 +- .../custom_components/test/sensor.py | 44 +++ 10 files changed, 689 insertions(+), 11 deletions(-) create mode 100644 homeassistant/components/sensor/device_trigger.py create mode 100644 homeassistant/components/sensor/strings.json create mode 100644 tests/components/sensor/test_device_trigger.py create mode 100644 tests/testing_config/custom_components/test/sensor.py diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 9dd4657291d..8d88fe9cae6 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -40,7 +40,9 @@ TRIGGER_SCHEMA = vol.All( _LOGGER = logging.getLogger(__name__) -async def async_attach_trigger(hass, config, action, automation_info): +async def async_attach_trigger( + hass, config, action, automation_info, *, platform_type="numeric_state" +): """Listen for state changes based on configuration.""" entity_id = config.get(CONF_ENTITY_ID) below = config.get(CONF_BELOW) @@ -84,7 +86,7 @@ async def async_attach_trigger(hass, config, action, automation_info): action( { "trigger": { - "platform": "numeric_state", + "platform": platform_type, "entity_id": entity, "below": below, "above": above, diff --git a/homeassistant/components/binary_sensor/device_trigger.py b/homeassistant/components/binary_sensor/device_trigger.py index c4d2efcb63b..89fd9add69a 100644 --- a/homeassistant/components/binary_sensor/device_trigger.py +++ b/homeassistant/components/binary_sensor/device_trigger.py @@ -195,8 +195,8 @@ async def async_attach_trigger(hass, config, action, automation_info): state_automation.CONF_FROM: from_state, state_automation.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state_automation.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" @@ -215,7 +215,7 @@ async def async_get_triggers(hass, device_id): ] for entry in entries: - device_class = None + device_class = DEVICE_CLASS_NONE state = hass.states.get(entry.entity_id) if state: device_class = state.attributes.get(ATTR_DEVICE_CLASS) diff --git a/homeassistant/components/device_automation/toggle_entity.py b/homeassistant/components/device_automation/toggle_entity.py index 7c68be83ba3..c9588c1efa7 100644 --- a/homeassistant/components/device_automation/toggle_entity.py +++ b/homeassistant/components/device_automation/toggle_entity.py @@ -155,8 +155,8 @@ async def async_attach_trigger( state.CONF_FROM: from_state, state.CONF_TO: to_state, } - if "for" in config: - state_config["for"] = config["for"] + if CONF_FOR in config: + state_config[CONF_FOR] = config[CONF_FOR] return await state.async_attach_trigger( hass, state_config, action, automation_info, platform_type="device" diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py new file mode 100644 index 00000000000..1074236eedf --- /dev/null +++ b/homeassistant/components/sensor/device_trigger.py @@ -0,0 +1,145 @@ +"""Provides device triggers for sensors.""" +import voluptuous as vol + +import homeassistant.components.automation.numeric_state as numeric_state_automation +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + CONF_ABOVE, + CONF_BELOW, + CONF_ENTITY_ID, + CONF_FOR, + CONF_TYPE, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, +) +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers import config_validation as cv + +from . import DOMAIN + + +# mypy: allow-untyped-defs, no-check-untyped-defs + +DEVICE_CLASS_NONE = "none" + +CONF_BATTERY_LEVEL = "battery_level" +CONF_HUMIDITY = "humidity" +CONF_ILLUMINANCE = "illuminance" +CONF_POWER = "power" +CONF_PRESSURE = "pressure" +CONF_SIGNAL_STRENGTH = "signal_strength" +CONF_TEMPERATURE = "temperature" +CONF_TIMESTAMP = "timestamp" +CONF_VALUE = "value" + +ENTITY_TRIGGERS = { + DEVICE_CLASS_BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], + DEVICE_CLASS_HUMIDITY: [{CONF_TYPE: CONF_HUMIDITY}], + DEVICE_CLASS_ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], + DEVICE_CLASS_POWER: [{CONF_TYPE: CONF_POWER}], + DEVICE_CLASS_PRESSURE: [{CONF_TYPE: CONF_PRESSURE}], + DEVICE_CLASS_SIGNAL_STRENGTH: [{CONF_TYPE: CONF_SIGNAL_STRENGTH}], + DEVICE_CLASS_TEMPERATURE: [{CONF_TYPE: CONF_TEMPERATURE}], + DEVICE_CLASS_TIMESTAMP: [{CONF_TYPE: CONF_TIMESTAMP}], + DEVICE_CLASS_NONE: [{CONF_TYPE: CONF_VALUE}], +} + + +TRIGGER_SCHEMA = vol.All( + TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In( + [ + CONF_BATTERY_LEVEL, + CONF_HUMIDITY, + CONF_ILLUMINANCE, + CONF_POWER, + CONF_PRESSURE, + CONF_SIGNAL_STRENGTH, + CONF_TEMPERATURE, + CONF_TIMESTAMP, + CONF_VALUE, + ] + ), + vol.Optional(CONF_BELOW): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_ABOVE): vol.Any(vol.Coerce(float)), + vol.Optional(CONF_FOR): vol.Any( + vol.All(cv.time_period, cv.positive_timedelta), + cv.template, + cv.template_complex, + ), + vol.Optional(CONF_FOR): cv.positive_time_period_dict, + } + ), + cv.has_at_least_one_key(CONF_BELOW, CONF_ABOVE), +) + + +async def async_attach_trigger(hass, config, action, automation_info): + """Listen for state changes based on configuration.""" + numeric_state_config = { + numeric_state_automation.CONF_ENTITY_ID: config[CONF_ENTITY_ID], + numeric_state_automation.CONF_ABOVE: config.get(CONF_ABOVE), + numeric_state_automation.CONF_BELOW: config.get(CONF_BELOW), + numeric_state_automation.CONF_FOR: config.get(CONF_FOR), + } + if CONF_FOR in config: + numeric_state_config[CONF_FOR] = config[CONF_FOR] + + return await numeric_state_automation.async_attach_trigger( + hass, numeric_state_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers.""" + triggers = [] + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = [ + entry + for entry in async_entries_for_device(entity_registry, device_id) + if entry.domain == DOMAIN + ] + + for entry in entries: + device_class = DEVICE_CLASS_NONE + state = hass.states.get(entry.entity_id) + if state: + device_class = state.attributes.get(ATTR_DEVICE_CLASS) + + templates = ENTITY_TRIGGERS.get( + device_class, ENTITY_TRIGGERS[DEVICE_CLASS_NONE] + ) + + triggers.extend( + ( + { + **automation, + "platform": "device", + "device_id": device_id, + "entity_id": entry.entity_id, + "domain": DOMAIN, + } + for automation in templates + ) + ) + + return triggers + + +async def async_get_trigger_capabilities(hass, trigger): + """List trigger capabilities.""" + return { + "extra_fields": vol.Schema( + {vol.Optional(CONF_FOR): cv.positive_time_period_dict} + ) + } diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json new file mode 100644 index 00000000000..7df239facde --- /dev/null +++ b/homeassistant/components/sensor/strings.json @@ -0,0 +1,26 @@ +{ + "device_automation": { + "condition_type": { + "is_battery_level": "{entity_name} battery level", + "is_humidity": "{entity_name} humidity", + "is_illuminance": "{entity_name} illuminance", + "is_power": "{entity_name} power", + "is_pressure": "{entity_name} pressure", + "is_signal_strength": "{entity_name} signal strength", + "is_temperature": "{entity_name} temperature", + "is_timestamp": "{entity_name} timestamp", + "is_value": "{entity_name} value" + }, + "trigger_type": { + "battery_level": "{entity_name} battery level", + "humidity": "{entity_name} humidity", + "illuminance": "{entity_name} illuminance", + "power": "{entity_name} power", + "pressure": "{entity_name} pressure", + "signal_strength": "{entity_name} signal strength", + "temperature": "{entity_name} temperature", + "timestamp": "{entity_name} timestamp", + "value": "{entity_name} value" + } + } +} diff --git a/tests/common.py b/tests/common.py index bd611e04c37..0684e6daafc 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1,5 +1,6 @@ """Test the helper method for writing tests.""" import asyncio +import collections import functools as ft import json import logging @@ -1050,3 +1051,85 @@ def async_mock_signal(hass, signal): hass.helpers.dispatcher.async_dispatcher_connect(signal, mock_signal_handler) return calls + + +class hashdict(dict): + """ + hashable dict implementation, suitable for use as a key into other dicts. + + >>> h1 = hashdict({"apples": 1, "bananas":2}) + >>> h2 = hashdict({"bananas": 3, "mangoes": 5}) + >>> h1+h2 + hashdict(apples=1, bananas=3, mangoes=5) + >>> d1 = {} + >>> d1[h1] = "salad" + >>> d1[h1] + 'salad' + >>> d1[h2] + Traceback (most recent call last): + ... + KeyError: hashdict(bananas=3, mangoes=5) + + based on answers from + http://stackoverflow.com/questions/1151658/python-hashable-dicts + + """ + + def __key(self): # noqa: D105 no docstring + return tuple(sorted(self.items())) + + def __repr__(self): # noqa: D105 no docstring + return ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()) + + def __hash__(self): # noqa: D105 no docstring + return hash(self.__key()) + + def __setitem__(self, key, value): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def __delitem__(self, key): # noqa: D105 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def clear(self): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def pop(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def popitem(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def setdefault(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + def update(self, *args, **kwargs): # noqa: D102 no docstring + raise TypeError( + "{0} does not support item assignment".format(self.__class__.__name__) + ) + + # update is not ok because it mutates the object + # __add__ is ok because it creates a new object + # while the new object is under construction, it's ok to mutate it + def __add__(self, right): # noqa: D105 no docstring + result = hashdict(self) + dict.update(result, right) + return result + + +def assert_lists_same(a, b): + """Compare two lists, ignoring order.""" + assert collections.Counter([hashdict(i) for i in a]) == collections.Counter( + [hashdict(i) for i in b] + ) diff --git a/tests/components/deconz/test_device_trigger.py b/tests/components/deconz/test_device_trigger.py index 4677ea8d5a7..91714e647bd 100644 --- a/tests/components/deconz/test_device_trigger.py +++ b/tests/components/deconz/test_device_trigger.py @@ -3,7 +3,7 @@ from copy import deepcopy from homeassistant.components.deconz import device_trigger -from tests.common import async_get_device_automations +from tests.common import assert_lists_same, async_get_device_automations from .test_gateway import ENTRY_CONFIG, DECONZ_WEB_REQUEST, setup_deconz_integration @@ -83,6 +83,13 @@ async def test_get_triggers(hass): "type": device_trigger.CONF_LONG_RELEASE, "subtype": device_trigger.CONF_TURN_OFF, }, + { + "device_id": device_id, + "domain": "sensor", + "entity_id": "sensor.tradfri_on_off_switch_battery_level", + "platform": "device", + "type": "battery_level", + }, ] - assert triggers == expected_triggers + assert_lists_same(triggers, expected_triggers) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py new file mode 100644 index 00000000000..1dc41f5bffa --- /dev/null +++ b/tests/components/sensor/test_device_trigger.py @@ -0,0 +1,368 @@ +"""The test for sensor device automation.""" +from datetime import timedelta +import pytest + +from homeassistant.components.sensor import DOMAIN, DEVICE_CLASSES +from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS +from homeassistant.const import STATE_UNKNOWN, CONF_PLATFORM +from homeassistant.setup import async_setup_component +import homeassistant.components.automation as automation +from homeassistant.helpers import device_registry +import homeassistant.util.dt as dt_util + +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + async_mock_service, + mock_device_registry, + mock_registry, + async_get_device_automations, + async_get_device_automation_capabilities, +) + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock serivce.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_triggers(hass, device_reg, entity_reg): + """Test we get the expected triggers from a sensor.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + for device_class in DEVICE_CLASSES: + entity_reg.async_get_or_create( + DOMAIN, + "test", + platform.ENTITIES[device_class].unique_id, + device_id=device_entry.id, + ) + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + expected_triggers = [ + { + "platform": "device", + "domain": DOMAIN, + "type": trigger["type"], + "device_id": device_entry.id, + "entity_id": platform.ENTITIES[device_class].entity_id, + } + for device_class in DEVICE_CLASSES + for trigger in ENTITY_TRIGGERS[device_class] + ] + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers == expected_triggers + + +async def test_get_trigger_capabilities(hass, device_reg, entity_reg): + """Test we get the expected capabilities from a binary_sensor trigger.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_capabilities = { + "extra_fields": [ + {"name": "for", "optional": True, "type": "positive_time_period_dict"} + ] + } + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + for trigger in triggers: + capabilities = await async_get_device_automation_capabilities( + hass, "trigger", trigger + ) + assert capabilities == expected_capabilities + + +async def test_if_fires_not_on_above_below(hass, calls, caplog): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + }, + "action": {"service": "test.automation"}, + } + ] + }, + ) + assert "must contain at least one of below, above" in caplog.text + + +async def test_if_fires_on_state_above(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_below(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "below": 10, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 11 - 9 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_between(hass, calls): + """Test for value triggers firing.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "below": 20, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "bat_low {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 9) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "bat_low device - {} - 9 - 11 - None".format( + sensor1.entity_id + ) + + hass.states.async_set(sensor1.entity_id, 21) + await hass.async_block_till_done() + assert len(calls) == 1 + + hass.states.async_set(sensor1.entity_id, 19) + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "bat_low device - {} - 21 - 19 - None".format( + sensor1.entity_id + ) + + +async def test_if_fires_on_state_change_with_for(hass, calls): + """Test for triggers firing with delay.""" + platform = getattr(hass.components, f"test.{DOMAIN}") + + platform.init() + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) + + sensor1 = platform.ENTITIES["battery"] + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": sensor1.entity_id, + "type": "battery_level", + "above": 10, + "for": {"seconds": 5}, + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "turn_off {{ trigger.%s }}" + % "}} - {{ trigger.".join( + ( + "platform", + "entity_id", + "from_state.state", + "to_state.state", + "for", + ) + ) + }, + }, + } + ] + }, + ) + await hass.async_block_till_done() + assert hass.states.get(sensor1.entity_id).state == STATE_UNKNOWN + assert len(calls) == 0 + + hass.states.async_set(sensor1.entity_id, 11) + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert len(calls) == 1 + await hass.async_block_till_done() + assert calls[0].data[ + "some" + ] == "turn_off device - {} - unknown - 11 - 0:00:05".format(sensor1.entity_id) diff --git a/tests/conftest.py b/tests/conftest.py index 36c0f52f41a..5e1bbc76fb5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,8 @@ from homeassistant.util import location from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.providers import legacy_api_password, homeassistant -from tests.common import ( +pytest.register_assert_rewrite("tests.common") +from tests.common import ( # noqa: E402 module level import not at top of file async_test_home_assistant, INSTANCES, mock_coro, @@ -21,7 +22,9 @@ from tests.common import ( MockUser, CLIENT_ID, ) -from tests.test_util.aiohttp import mock_aiohttp_client +from tests.test_util.aiohttp import ( + mock_aiohttp_client, +) # noqa: E402 module level import not at top of file if os.environ.get("UVLOOP") == "1": import uvloop diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/testing_config/custom_components/test/sensor.py new file mode 100644 index 00000000000..c25be28fdb0 --- /dev/null +++ b/tests/testing_config/custom_components/test/sensor.py @@ -0,0 +1,44 @@ +""" +Provide a mock sensor platform. + +Call init before using it in your tests to ensure clean test data. +""" +from homeassistant.components.sensor import DEVICE_CLASSES +from tests.common import MockEntity + + +ENTITIES = {} + + +def init(empty=False): + """Initialize the platform with entities.""" + global ENTITIES + + ENTITIES = ( + {} + if empty + else { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + ) + for device_class in DEVICE_CLASSES + } + ) + + +async def async_setup_platform( + hass, config, async_add_entities_callback, discovery_info=None +): + """Return mock entities.""" + async_add_entities_callback(list(ENTITIES.values())) + + +class MockSensor(MockEntity): + """Mock Sensor class.""" + + @property + def device_class(self): + """Return the class of this sensor.""" + return self._handle("device_class")