Add Emulated Kasa Integration (#39630)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Keilin Bickar 2020-09-05 08:57:45 -04:00 committed by GitHub
parent 8567fe94e1
commit 3022fc4702
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 665 additions and 3 deletions

View file

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

View file

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

View file

@ -0,0 +1,5 @@
"""Constants for emulated_kasa."""
CONF_POWER = "power"
CONF_POWER_ENTITY = "power_entity"
DOMAIN = "emulated_kasa"

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1 @@
"""Tests for emulated_kasa."""

View file

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