Add Emulated Kasa Integration (#39630)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
8567fe94e1
commit
3022fc4702
9 changed files with 665 additions and 3 deletions
|
@ -120,6 +120,7 @@ homeassistant/components/elkm1/* @gwww @bdraco
|
||||||
homeassistant/components/elv/* @majuss
|
homeassistant/components/elv/* @majuss
|
||||||
homeassistant/components/emby/* @mezz64
|
homeassistant/components/emby/* @mezz64
|
||||||
homeassistant/components/emoncms/* @borpin
|
homeassistant/components/emoncms/* @borpin
|
||||||
|
homeassistant/components/emulated_kasa/* @kbickar
|
||||||
homeassistant/components/enigma2/* @fbradyirl
|
homeassistant/components/enigma2/* @fbradyirl
|
||||||
homeassistant/components/enocean/* @bdurrer
|
homeassistant/components/enocean/* @bdurrer
|
||||||
homeassistant/components/entur_public_transport/* @hfurubotten
|
homeassistant/components/entur_public_transport/* @hfurubotten
|
||||||
|
|
150
homeassistant/components/emulated_kasa/__init__.py
Normal file
150
homeassistant/components/emulated_kasa/__init__.py
Normal 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,
|
||||||
|
)
|
5
homeassistant/components/emulated_kasa/const.py
Normal file
5
homeassistant/components/emulated_kasa/const.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
"""Constants for emulated_kasa."""
|
||||||
|
|
||||||
|
CONF_POWER = "power"
|
||||||
|
CONF_POWER_ENTITY = "power_entity"
|
||||||
|
DOMAIN = "emulated_kasa"
|
8
homeassistant/components/emulated_kasa/manifest.json
Normal file
8
homeassistant/components/emulated_kasa/manifest.json
Normal 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"
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
"domain": "sense",
|
"domain": "sense",
|
||||||
"name": "Sense",
|
"name": "Sense",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||||
"requirements": ["sense_energy==0.7.2"],
|
"requirements": ["sense_energy==0.8.0"],
|
||||||
"codeowners": ["@kbickar"],
|
"codeowners": ["@kbickar"],
|
||||||
"config_flow": true
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1955,8 +1955,9 @@ sendgrid==6.4.6
|
||||||
# homeassistant.components.sensehat
|
# homeassistant.components.sensehat
|
||||||
sense-hat==2.2.0
|
sense-hat==2.2.0
|
||||||
|
|
||||||
|
# homeassistant.components.emulated_kasa
|
||||||
# homeassistant.components.sense
|
# homeassistant.components.sense
|
||||||
sense_energy==0.7.2
|
sense_energy==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.sentry
|
# homeassistant.components.sentry
|
||||||
sentry-sdk==0.17.3
|
sentry-sdk==0.17.3
|
||||||
|
|
|
@ -907,8 +907,9 @@ samsungctl[websocket]==0.7.1
|
||||||
# homeassistant.components.samsungtv
|
# homeassistant.components.samsungtv
|
||||||
samsungtvws[websocket]==1.4.0
|
samsungtvws[websocket]==1.4.0
|
||||||
|
|
||||||
|
# homeassistant.components.emulated_kasa
|
||||||
# homeassistant.components.sense
|
# homeassistant.components.sense
|
||||||
sense_energy==0.7.2
|
sense_energy==0.8.0
|
||||||
|
|
||||||
# homeassistant.components.sentry
|
# homeassistant.components.sentry
|
||||||
sentry-sdk==0.17.3
|
sentry-sdk==0.17.3
|
||||||
|
|
1
tests/components/emulated_kasa/__init__.py
Normal file
1
tests/components/emulated_kasa/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for emulated_kasa."""
|
495
tests/components/emulated_kasa/test_init.py
Normal file
495
tests/components/emulated_kasa/test_init.py
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue