diff --git a/CODEOWNERS b/CODEOWNERS index e3b0f0462c3..0f48fcc3dcc 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -120,6 +120,7 @@ homeassistant/components/elkm1/* @gwww @bdraco homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin +homeassistant/components/emulated_kasa/* @kbickar homeassistant/components/enigma2/* @fbradyirl homeassistant/components/enocean/* @bdurrer homeassistant/components/entur_public_transport/* @hfurubotten diff --git a/homeassistant/components/emulated_kasa/__init__.py b/homeassistant/components/emulated_kasa/__init__.py new file mode 100644 index 00000000000..b9dc79e25cc --- /dev/null +++ b/homeassistant/components/emulated_kasa/__init__.py @@ -0,0 +1,150 @@ +"""Support for local power state reporting of entities by emulating TP-Link Kasa smart plugs.""" +import logging + +from sense_energy import PlugInstance, SenseLink +import voluptuous as vol + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import ATTR_CURRENT_POWER_W +from homeassistant.const import ( + CONF_ENTITIES, + CONF_NAME, + CONF_UNIQUE_ID, + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + STATE_ON, +) +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity_registry import RegistryEntry +from homeassistant.helpers.template import Template, is_template_string + +from .const import CONF_POWER, CONF_POWER_ENTITY, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +CONFIG_ENTITY_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_POWER): vol.Any( + vol.Coerce(float), + cv.template, + ), + vol.Optional(CONF_POWER_ENTITY): cv.string, + } +) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_ENTITIES): vol.Schema( + {cv.entity_id: CONFIG_ENTITY_SCHEMA} + ), + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the emulated_kasa component.""" + conf = config.get(DOMAIN) + if not conf: + return True + entity_configs = conf[CONF_ENTITIES] + + def devices(): + """Devices to be emulated.""" + yield from get_plug_devices(hass, entity_configs) + + server = SenseLink(devices) + + async def stop_emulated_kasa(event): + await server.stop() + + async def start_emulated_kasa(event): + await validate_configs(hass, entity_configs) + try: + await server.start() + except OSError as error: + _LOGGER.error("Failed to create UDP server at port 9999: %s", error) + else: + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_emulated_kasa) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, start_emulated_kasa) + + return True + + +async def validate_configs(hass, entity_configs): + """Validate that entities exist and ensure templates are ready to use.""" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + for entity_id, entity_config in entity_configs.items(): + state = hass.states.get(entity_id) + if state is None: + _LOGGER.debug("Entity not found: %s", entity_id) + continue + + entity = entity_registry.async_get(entity_id) + if entity: + entity_config[CONF_UNIQUE_ID] = get_system_unique_id(entity) + else: + entity_config[CONF_UNIQUE_ID] = entity_id + + if CONF_POWER in entity_config: + power_val = entity_config[CONF_POWER] + if isinstance(power_val, str) and is_template_string(power_val): + entity_config[CONF_POWER] = Template(power_val, hass) + elif isinstance(power_val, Template): + entity_config[CONF_POWER].hass = hass + elif CONF_POWER_ENTITY in entity_config: + power_val = entity_config[CONF_POWER_ENTITY] + if hass.states.get(power_val) is None: + _LOGGER.debug("Sensor Entity not found: %s", power_val) + else: + entity_config[CONF_POWER] = power_val + elif state.domain == SENSOR_DOMAIN: + pass + elif ATTR_CURRENT_POWER_W in state.attributes: + pass + else: + _LOGGER.debug("No power value defined for: %s", entity_id) + + +def get_system_unique_id(entity: RegistryEntry): + """Determine the system wide unique_id for an entity.""" + return f"{entity.platform}.{entity.domain}.{entity.unique_id}" + + +def get_plug_devices(hass, entity_configs): + """Produce list of plug devices from config entities.""" + for entity_id, entity_config in entity_configs.items(): + state = hass.states.get(entity_id) + if state is None: + continue + name = entity_config.get(CONF_NAME, state.name) + + if state.state == STATE_ON or state.domain == SENSOR_DOMAIN: + if CONF_POWER in entity_config: + power_val = entity_config[CONF_POWER] + if isinstance(power_val, (float, int)): + power = float(power_val) + elif isinstance(power_val, str): + power = float(hass.states.get(power_val).state) + elif isinstance(power_val, Template): + power = float(power_val.async_render()) + elif ATTR_CURRENT_POWER_W in state.attributes: + power = float(state.attributes[ATTR_CURRENT_POWER_W]) + elif state.domain == SENSOR_DOMAIN: + power = float(state.state) + else: + power = 0.0 + last_changed = state.last_changed.timestamp() + yield PlugInstance( + entity_config[CONF_UNIQUE_ID], + start_time=last_changed, + alias=name, + power=power, + ) diff --git a/homeassistant/components/emulated_kasa/const.py b/homeassistant/components/emulated_kasa/const.py new file mode 100644 index 00000000000..967cf90d331 --- /dev/null +++ b/homeassistant/components/emulated_kasa/const.py @@ -0,0 +1,5 @@ +"""Constants for emulated_kasa.""" + +CONF_POWER = "power" +CONF_POWER_ENTITY = "power_entity" +DOMAIN = "emulated_kasa" diff --git a/homeassistant/components/emulated_kasa/manifest.json b/homeassistant/components/emulated_kasa/manifest.json new file mode 100644 index 00000000000..678b04bc5c0 --- /dev/null +++ b/homeassistant/components/emulated_kasa/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "emulated_kasa", + "name": "Emulated Kasa", + "documentation": "https://www.home-assistant.io/integrations/emulated_kasa", + "requirements": ["sense_energy==0.8.0"], + "codeowners": ["@kbickar"], + "quality_scale": "internal" +} diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index deadf759f06..c0c568a8e3d 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -2,7 +2,7 @@ "domain": "sense", "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", - "requirements": ["sense_energy==0.7.2"], + "requirements": ["sense_energy==0.8.0"], "codeowners": ["@kbickar"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 83d8503bf3f..93590f7b386 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1955,8 +1955,9 @@ sendgrid==6.4.6 # homeassistant.components.sensehat sense-hat==2.2.0 +# homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.7.2 +sense_energy==0.8.0 # homeassistant.components.sentry sentry-sdk==0.17.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8d03da1e001..75249673c2c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -907,8 +907,9 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws[websocket]==1.4.0 +# homeassistant.components.emulated_kasa # homeassistant.components.sense -sense_energy==0.7.2 +sense_energy==0.8.0 # homeassistant.components.sentry sentry-sdk==0.17.3 diff --git a/tests/components/emulated_kasa/__init__.py b/tests/components/emulated_kasa/__init__.py new file mode 100644 index 00000000000..a762eeca16e --- /dev/null +++ b/tests/components/emulated_kasa/__init__.py @@ -0,0 +1 @@ +"""Tests for emulated_kasa.""" diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py new file mode 100644 index 00000000000..10ccb4d68a5 --- /dev/null +++ b/tests/components/emulated_kasa/test_init.py @@ -0,0 +1,495 @@ +"""Tests for emulated_kasa library bindings.""" +import math + +from homeassistant.components import emulated_kasa +from homeassistant.components.emulated_kasa.const import ( + CONF_POWER, + CONF_POWER_ENTITY, + DOMAIN, +) +from homeassistant.components.fan import ( + ATTR_SPEED, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_SPEED, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import ( + ATTR_CURRENT_POWER_W, + DOMAIN as SWITCH_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + CONF_ENTITIES, + CONF_NAME, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, Mock, patch + +ENTITY_SWITCH = "switch.ac" +ENTITY_SWITCH_NAME = "A/C" +ENTITY_SWITCH_POWER = 400.0 +ENTITY_LIGHT = "light.bed_light" +ENTITY_LIGHT_NAME = "Bed Room Lights" +ENTITY_FAN = "fan.ceiling_fan" +ENTITY_FAN_NAME = "Ceiling Fan" +ENTITY_FAN_SPEED_LOW = 5 +ENTITY_FAN_SPEED_MED = 10 +ENTITY_FAN_SPEED_HIGH = 50 +ENTITY_SENSOR = "sensor.outside_temperature" +ENTITY_SENSOR_NAME = "Power Sensor" + +CONFIG = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: { + CONF_NAME: ENTITY_SWITCH_NAME, + CONF_POWER: ENTITY_SWITCH_POWER, + }, + ENTITY_LIGHT: { + CONF_NAME: ENTITY_LIGHT_NAME, + CONF_POWER_ENTITY: ENTITY_SENSOR, + }, + ENTITY_FAN: { + CONF_POWER: "{% if is_state_attr('" + + ENTITY_FAN + + "','speed', 'low') %} " + + str(ENTITY_FAN_SPEED_LOW) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'medium') %} " + + str(ENTITY_FAN_SPEED_MED) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'high') %} " + + str(ENTITY_FAN_SPEED_HIGH) + + "{% endif %}" + }, + } + } +} + +CONFIG_SWITCH = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: { + CONF_NAME: ENTITY_SWITCH_NAME, + CONF_POWER: ENTITY_SWITCH_POWER, + }, + } + } +} + +CONFIG_SWITCH_NO_POWER = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SWITCH: {}, + } + } +} + +CONFIG_LIGHT = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_LIGHT: { + CONF_NAME: ENTITY_LIGHT_NAME, + CONF_POWER_ENTITY: ENTITY_SENSOR, + }, + } + } +} + +CONFIG_FAN = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_FAN: { + CONF_POWER: "{% if is_state_attr('" + + ENTITY_FAN + + "','speed', 'low') %} " + + str(ENTITY_FAN_SPEED_LOW) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'medium') %} " + + str(ENTITY_FAN_SPEED_MED) + + "{% elif is_state_attr('" + + ENTITY_FAN + + "','speed', 'high') %} " + + str(ENTITY_FAN_SPEED_HIGH) + + "{% endif %}" + }, + } + } +} + +CONFIG_SENSOR = { + DOMAIN: { + CONF_ENTITIES: { + ENTITY_SENSOR: {CONF_NAME: ENTITY_SENSOR_NAME}, + } + } +} + + +def nested_value(ndict, *keys): + """Return a nested dict value or None if it doesn't exist.""" + if len(keys) == 0: + return ndict + key = keys[0] + if not isinstance(ndict, dict) or key not in ndict: + return None + return nested_value(ndict[key], *keys[1:]) + + +async def test_setup(hass): + """Test that devices are reported correctly.""" + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG) is True + + +async def test_float(hass): + """Test a configuration using a simple float.""" + config = CONFIG_SWITCH[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SWITCH) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn switch on + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_SWITCH_POWER) + + # Turn off + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_switch_power(hass): + """Test a configuration using a simple float.""" + config = CONFIG_SWITCH_NO_POWER[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SWITCH_DOMAIN, + {SWITCH_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SWITCH_NO_POWER) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn switch on + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + power = switch.attributes[ATTR_CURRENT_POWER_W] + assert power == 100 + assert switch.name == "AC" + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, power) + + hass.states.async_set( + ENTITY_SWITCH, + STATE_ON, + attributes={ATTR_CURRENT_POWER_W: 120, ATTR_FRIENDLY_NAME: "AC"}, + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 120) + + # Turn off + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == "AC" + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_template(hass): + """Test a configuration using a complex template.""" + config = CONFIG_FAN[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, FAN_DOMAIN, {FAN_DOMAIN: {"platform": "demo"}} + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_FAN) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn all devices on to known state + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "low"}, + blocking=True, + ) + + fan = hass.states.get(ENTITY_FAN) + assert fan.state == STATE_ON + + # Fan low: + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_LOW) + + # Fan High: + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "high"}, + blocking=True, + ) + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_HIGH) + + # Fan off: + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_sensor(hass): + """Test a configuration using a sensor in a template.""" + config = CONFIG_LIGHT[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_LIGHT) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + hass.states.async_set(ENTITY_SENSOR, 35) + + light = hass.states.get(ENTITY_LIGHT) + assert light.state == STATE_ON + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + + # light + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # change power sensor + hass.states.async_set(ENTITY_SENSOR, 40) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 40) + + # report 0 if device is off + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_sensor_state(hass): + """Test a configuration using a sensor in a template.""" + config = CONFIG_SENSOR[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await async_setup_component(hass, DOMAIN, CONFIG_SENSOR) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + hass.states.async_set(ENTITY_SENSOR, 35) + + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + + # sensor + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # change power sensor + hass.states.async_set(ENTITY_SENSOR, 40) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 40) + + # report 0 if device is off + hass.states.async_set(ENTITY_SENSOR, 0) + + plug_it = emulated_kasa.get_plug_devices(hass, config) + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SENSOR_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 0) + + +async def test_multiple_devices(hass): + """Test that devices are reported correctly.""" + config = CONFIG[DOMAIN][CONF_ENTITIES] + assert await async_setup_component( + hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, FAN_DOMAIN, {FAN_DOMAIN: {"platform": "demo"}} + ) + assert await async_setup_component( + hass, + SENSOR_DOMAIN, + {SENSOR_DOMAIN: {"platform": "demo"}}, + ) + with patch( + "sense_energy.SenseLink", + return_value=Mock(start=AsyncMock(), close=AsyncMock()), + ): + assert await emulated_kasa.async_setup(hass, CONFIG) is True + await hass.async_block_till_done() + await emulated_kasa.validate_configs(hass, config) + + # Turn all devices on to known state + await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_SWITCH}, blocking=True + ) + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_LIGHT}, blocking=True + ) + hass.states.async_set(ENTITY_SENSOR, 35) + await hass.services.async_call( + FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_FAN}, blocking=True + ) + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_ENTITY_ID: ENTITY_FAN, ATTR_SPEED: "medium"}, + blocking=True, + ) + + # All of them should now be on + switch = hass.states.get(ENTITY_SWITCH) + assert switch.state == STATE_ON + light = hass.states.get(ENTITY_LIGHT) + assert light.state == STATE_ON + sensor = hass.states.get(ENTITY_SENSOR) + assert sensor.state == "35" + fan = hass.states.get(ENTITY_FAN) + assert fan.state == STATE_ON + + plug_it = emulated_kasa.get_plug_devices(hass, config) + # switch + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_SWITCH_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_SWITCH_POWER) + + # light + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_LIGHT_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, 35) + + # fan + plug = next(plug_it).generate_response() + assert nested_value(plug, "system", "get_sysinfo", "alias") == ENTITY_FAN_NAME + power = nested_value(plug, "emeter", "get_realtime", "power") + assert math.isclose(power, ENTITY_FAN_SPEED_MED) + + # No more devices + assert next(plug_it, None) is None