Insteon Device Control Panel (#70834)

Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
Tom Harris 2022-04-28 15:35:43 -04:00 committed by GitHub
parent 9af8cd030a
commit a9ca774e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 650 additions and 519 deletions

View File

@ -9,11 +9,13 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP from homeassistant.const import CONF_PLATFORM, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from . import api from . import api
from .const import ( from .const import (
CONF_CAT, CONF_CAT,
CONF_DEV_PATH,
CONF_DIM_STEPS, CONF_DIM_STEPS,
CONF_HOUSECODE, CONF_HOUSECODE,
CONF_OVERRIDE, CONF_OVERRIDE,
@ -74,13 +76,19 @@ async def close_insteon_connection(*args):
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Insteon platform.""" """Set up the Insteon platform."""
hass.data[DOMAIN] = {}
if DOMAIN not in config: if DOMAIN not in config:
return True return True
conf = config[DOMAIN] conf = dict(config[DOMAIN])
hass.data[DOMAIN][CONF_DEV_PATH] = conf.pop(CONF_DEV_PATH, None)
if not conf:
return True
data, options = convert_yaml_to_config_flow(conf) data, options = convert_yaml_to_config_flow(conf)
if options: if options:
hass.data[DOMAIN] = {}
hass.data[DOMAIN][OPTIONS] = options hass.data[DOMAIN][OPTIONS] = options
# Create a config entry with the connection data # Create a config entry with the connection data
hass.async_create_task( hass.async_create_task(
@ -154,23 +162,30 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
platforms = get_device_platforms(device) platforms = get_device_platforms(device)
if ON_OFF_EVENTS in platforms: if ON_OFF_EVENTS in platforms:
add_on_off_event_device(hass, device) add_on_off_event_device(hass, device)
create_insteon_device(hass, device, entry.entry_id)
_LOGGER.debug("Insteon device count: %s", len(devices)) _LOGGER.debug("Insteon device count: %s", len(devices))
register_new_device_callback(hass) register_new_device_callback(hass)
async_register_services(hass) async_register_services(hass)
device_registry = await hass.helpers.device_registry.async_get_registry() create_insteon_device(hass, devices.modem, entry.entry_id)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, str(devices.modem.address))},
manufacturer="Smart Home",
name=f"{devices.modem.description} {devices.modem.address}",
model=f"{devices.modem.model} ({devices.modem.cat!r}, 0x{devices.modem.subcat:02x})",
sw_version=f"{devices.modem.firmware:02x} Engine Version: {devices.modem.engine_version}",
)
api.async_load_api(hass) api.async_load_api(hass)
await api.async_register_insteon_frontend(hass)
asyncio.create_task(async_get_device_config(hass, entry)) asyncio.create_task(async_get_device_config(hass, entry))
return True return True
def create_insteon_device(hass, device, config_entry_id):
"""Create an Insteon device."""
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=config_entry_id, # entry.entry_id,
identifiers={(DOMAIN, str(device.address))},
manufacturer="SmartLabs, Inc",
name=f"{device.description} {device.address}",
model=f"{device.model} ({device.cat!r}, 0x{device.subcat:02x})",
sw_version=f"{device.firmware:02x} Engine Version: {device.engine_version}",
)

View File

@ -1,7 +1,10 @@
"""Insteon API interface for the frontend.""" """Insteon API interface for the frontend."""
from homeassistant.components import websocket_api from insteon_frontend import get_build_id, locate_dir
from homeassistant.core import callback
from homeassistant.components import panel_custom, websocket_api
from homeassistant.components.insteon.const import CONF_DEV_PATH, DOMAIN
from homeassistant.core import HomeAssistant, callback
from .aldb import ( from .aldb import (
websocket_add_default_links, websocket_add_default_links,
@ -13,7 +16,11 @@ from .aldb import (
websocket_reset_aldb, websocket_reset_aldb,
websocket_write_aldb, websocket_write_aldb,
) )
from .device import websocket_get_device from .device import (
websocket_add_device,
websocket_cancel_add_device,
websocket_get_device,
)
from .properties import ( from .properties import (
websocket_change_properties_record, websocket_change_properties_record,
websocket_get_properties, websocket_get_properties,
@ -22,11 +29,15 @@ from .properties import (
websocket_write_properties, websocket_write_properties,
) )
URL_BASE = "/insteon_static"
@callback @callback
def async_load_api(hass): def async_load_api(hass):
"""Set up the web socket API.""" """Set up the web socket API."""
websocket_api.async_register_command(hass, websocket_get_device) websocket_api.async_register_command(hass, websocket_get_device)
websocket_api.async_register_command(hass, websocket_add_device)
websocket_api.async_register_command(hass, websocket_cancel_add_device)
websocket_api.async_register_command(hass, websocket_get_aldb) websocket_api.async_register_command(hass, websocket_get_aldb)
websocket_api.async_register_command(hass, websocket_change_aldb_record) websocket_api.async_register_command(hass, websocket_change_aldb_record)
@ -42,3 +53,31 @@ def async_load_api(hass):
websocket_api.async_register_command(hass, websocket_write_properties) websocket_api.async_register_command(hass, websocket_write_properties)
websocket_api.async_register_command(hass, websocket_load_properties) websocket_api.async_register_command(hass, websocket_load_properties)
websocket_api.async_register_command(hass, websocket_reset_properties) websocket_api.async_register_command(hass, websocket_reset_properties)
def get_entrypoint(is_dev):
"""Get the entry point for the frontend."""
if is_dev:
return "entrypoint.js"
async def async_register_insteon_frontend(hass: HomeAssistant):
"""Register the Insteon frontend configuration panel."""
# Add to sidepanel if needed
if DOMAIN not in hass.data.get("frontend_panels", {}):
dev_path = hass.data.get(DOMAIN, {}).get(CONF_DEV_PATH)
is_dev = dev_path is not None
path = dev_path if dev_path else locate_dir()
build_id = get_build_id(is_dev)
hass.http.register_static_path(URL_BASE, path, cache_headers=not is_dev)
await panel_custom.async_register_panel(
hass=hass,
frontend_url_path=DOMAIN,
webcomponent_name="insteon-frontend",
sidebar_title=DOMAIN.capitalize(),
sidebar_icon="mdi:power",
module_url=f"{URL_BASE}/entrypoint-{build_id}.js",
embed_iframe=True,
require_admin=True,
)

View File

@ -1,17 +1,20 @@
"""API interface to get an Insteon device.""" """API interface to get an Insteon device."""
from pyinsteon import devices from pyinsteon import devices
from pyinsteon.constants import DeviceAction
import voluptuous as vol import voluptuous as vol
from homeassistant.components import websocket_api from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, callback
from ..const import ( from ..const import (
DEVICE_ADDRESS,
DEVICE_ID, DEVICE_ID,
DOMAIN, DOMAIN,
HA_DEVICE_NOT_FOUND, HA_DEVICE_NOT_FOUND,
ID, ID,
INSTEON_DEVICE_NOT_FOUND, INSTEON_DEVICE_NOT_FOUND,
MULTIPLE,
TYPE, TYPE,
) )
@ -21,6 +24,12 @@ def compute_device_name(ha_device):
return ha_device.name_by_user if ha_device.name_by_user else ha_device.name return ha_device.name_by_user if ha_device.name_by_user else ha_device.name
async def async_add_devices(address, multiple):
"""Add one or more Insteon devices."""
async for _ in devices.async_add_device(address=address, multiple=multiple):
pass
def get_insteon_device_from_ha_device(ha_device): def get_insteon_device_from_ha_device(ha_device):
"""Return the Insteon device from an HA device.""" """Return the Insteon device from an HA device."""
for identifier in ha_device.identifiers: for identifier in ha_device.identifiers:
@ -74,3 +83,58 @@ async def websocket_get_device(
"aldb_status": str(device.aldb.status), "aldb_status": str(device.aldb.status),
} }
connection.send_result(msg[ID], device_info) connection.send_result(msg[ID], device_info)
@websocket_api.websocket_command(
{
vol.Required(TYPE): "insteon/device/add",
vol.Required(MULTIPLE): bool,
vol.Optional(DEVICE_ADDRESS): str,
}
)
@websocket_api.require_admin
@websocket_api.async_response
async def websocket_add_device(
hass: HomeAssistant,
connection: websocket_api.connection.ActiveConnection,
msg: dict,
) -> None:
"""Add one or more Insteon devices."""
@callback
def linking_complete(address: str, action: DeviceAction):
"""Forward device events to websocket."""
if action == DeviceAction.COMPLETED:
forward_data = {"type": "linking_stopped", "address": ""}
else:
return
connection.send_message(websocket_api.event_message(msg["id"], forward_data))
@callback
def async_cleanup() -> None:
"""Remove signal listeners."""
devices.unsubscribe(linking_complete)
connection.subscriptions[msg["id"]] = async_cleanup
devices.subscribe(linking_complete)
async for address in devices.async_add_device(
address=msg.get(DEVICE_ADDRESS), multiple=msg[MULTIPLE]
):
forward_data = {"type": "device_added", "address": str(address)}
connection.send_message(websocket_api.event_message(msg["id"], forward_data))
connection.send_result(msg[ID])
@websocket_api.websocket_command({vol.Required(TYPE): "insteon/device/add/cancel"})
@websocket_api.require_admin
@websocket_api.async_response
async def websocket_cancel_add_device(
hass: HomeAssistant,
connection: websocket_api.connection.ActiveConnection,
msg: dict,
) -> None:
"""Cancel the Insteon all-linking process."""
await devices.async_cancel_all_linking()
connection.send_result(msg[ID])

View File

@ -1,17 +1,15 @@
"""Property update methods and schemas.""" """Property update methods and schemas."""
from itertools import chain
from pyinsteon import devices from pyinsteon import devices
from pyinsteon.constants import RAMP_RATES, ResponseStatus from pyinsteon.config import RADIO_BUTTON_GROUPS, RAMP_RATE_IN_SEC, get_usable_value
from pyinsteon.device_types.device_base import Device from pyinsteon.constants import (
from pyinsteon.extended_property import ( RAMP_RATES_SEC,
NON_TOGGLE_MASK, PropertyType,
NON_TOGGLE_ON_OFF_MASK, RelayMode,
OFF_MASK, ResponseStatus,
ON_MASK, ToggleMode,
RAMP_RATE,
) )
from pyinsteon.utils import ramp_rate_to_seconds, seconds_to_ramp_rate from pyinsteon.device_types.device_base import Device
import voluptuous as vol import voluptuous as vol
import voluptuous_serialize import voluptuous_serialize
@ -29,19 +27,12 @@ from ..const import (
) )
from .device import notify_device_not_found from .device import notify_device_not_found
TOGGLE_ON_OFF_MODE = "toggle_on_off_mode" SHOW_ADVANCED = "show_advanced"
NON_TOGGLE_ON_MODE = "non_toggle_on_mode" RAMP_RATE_SECONDS = list(dict.fromkeys(RAMP_RATES_SEC))
NON_TOGGLE_OFF_MODE = "non_toggle_off_mode"
RADIO_BUTTON_GROUP_PROP = "radio_button_group_"
TOGGLE_PROP = "toggle_"
RAMP_RATE_SECONDS = list(dict.fromkeys(RAMP_RATES.values()))
RAMP_RATE_SECONDS.sort() RAMP_RATE_SECONDS.sort()
TOGGLE_MODES = {TOGGLE_ON_OFF_MODE: 0, NON_TOGGLE_ON_MODE: 1, NON_TOGGLE_OFF_MODE: 2} RAMP_RATE_LIST = [str(seconds) for seconds in RAMP_RATE_SECONDS]
TOGGLE_MODES_SCHEMA = { TOGGLE_MODES = [str(ToggleMode(v)).lower() for v in list(ToggleMode)]
0: TOGGLE_ON_OFF_MODE, RELAY_MODES = [str(RelayMode(v)).lower() for v in list(RelayMode)]
1: NON_TOGGLE_ON_MODE,
2: NON_TOGGLE_OFF_MODE,
}
def _bool_schema(name): def _bool_schema(name):
@ -52,239 +43,116 @@ def _byte_schema(name):
return voluptuous_serialize.convert(vol.Schema({vol.Required(name): cv.byte}))[0] return voluptuous_serialize.convert(vol.Schema({vol.Required(name): cv.byte}))[0]
def _ramp_rate_schema(name): def _float_schema(name):
return voluptuous_serialize.convert(vol.Schema({vol.Required(name): float}))[0]
def _list_schema(name, values):
return voluptuous_serialize.convert( return voluptuous_serialize.convert(
vol.Schema({vol.Required(name): vol.In(RAMP_RATE_SECONDS)}), vol.Schema({vol.Required(name): vol.In(values)}),
custom_serializer=cv.custom_serializer, custom_serializer=cv.custom_serializer,
)[0] )[0]
def get_properties(device: Device): def _multi_select_schema(name, values):
return voluptuous_serialize.convert(
vol.Schema({vol.Optional(name): cv.multi_select(values)}),
custom_serializer=cv.custom_serializer,
)[0]
def _read_only_schema(name, value):
"""Return a constant value schema."""
return voluptuous_serialize.convert(vol.Schema({vol.Required(name): value}))[0]
def get_schema(prop, name, groups):
"""Return the correct shema type."""
if prop.is_read_only:
return _read_only_schema(name, prop.value)
if name == RAMP_RATE_IN_SEC:
return _list_schema(name, RAMP_RATE_LIST)
if name == RADIO_BUTTON_GROUPS:
button_list = {str(group): groups[group].name for group in groups if group != 1}
return _multi_select_schema(name, button_list)
if prop.value_type == bool:
return _bool_schema(name)
if prop.value_type == int:
return _byte_schema(name)
if prop.value_type == float:
return _float_schema(name)
if prop.value_type == ToggleMode:
return _list_schema(name, TOGGLE_MODES)
if prop.value_type == RelayMode:
return _list_schema(name, RELAY_MODES)
return None
def get_properties(device: Device, show_advanced=False):
"""Get the properties of an Insteon device and return the records and schema.""" """Get the properties of an Insteon device and return the records and schema."""
properties = [] properties = []
schema = {} schema = {}
# Limit the properties we manage at this time. for name, prop in device.configuration.items():
for prop_name in device.operating_flags: if prop.is_read_only and not show_advanced:
if not device.operating_flags[prop_name].is_read_only:
prop_dict, schema_dict = _get_property(device.operating_flags[prop_name])
properties.append(prop_dict)
schema[prop_name] = schema_dict
mask_found = False
for prop_name in device.properties:
if device.properties[prop_name].is_read_only:
continue continue
if prop_name == RAMP_RATE: prop_schema = get_schema(prop, name, device.groups)
rr_prop, rr_schema = _get_ramp_rate_property(device.properties[prop_name]) if name == "momentary_delay":
properties.append(rr_prop) print(prop_schema)
schema[RAMP_RATE] = rr_schema if prop_schema is None:
continue
schema[name] = prop_schema
properties.append(property_to_dict(prop))
elif not mask_found and "mask" in prop_name: if show_advanced:
mask_found = True for name, prop in device.operating_flags.items():
toggle_props, toggle_schema = _get_toggle_properties(device) if prop.property_type != PropertyType.ADVANCED:
properties.extend(toggle_props) continue
schema.update(toggle_schema) prop_schema = get_schema(prop, name, device.groups)
if prop_schema is not None:
rb_props, rb_schema = _get_radio_button_properties(device) schema[name] = prop_schema
properties.extend(rb_props) properties.append(property_to_dict(prop))
schema.update(rb_schema) for name, prop in device.properties.items():
else: if prop.property_type != PropertyType.ADVANCED:
prop_dict, schema_dict = _get_property(device.properties[prop_name]) continue
properties.append(prop_dict) prop_schema = get_schema(prop, name, device.groups)
schema[prop_name] = schema_dict if prop_schema is not None:
schema[name] = prop_schema
properties.append(property_to_dict(prop))
return properties, schema return properties, schema
def set_property(device, prop_name: str, value): def property_to_dict(prop):
"""Update a property value."""
if isinstance(value, bool) and prop_name in device.operating_flags:
device.operating_flags[prop_name].new_value = value
elif prop_name == RAMP_RATE:
device.properties[prop_name].new_value = seconds_to_ramp_rate(value)
elif prop_name.startswith(RADIO_BUTTON_GROUP_PROP):
buttons = [int(button) for button in value]
rb_groups = _calc_radio_button_groups(device)
curr_group = int(prop_name[len(RADIO_BUTTON_GROUP_PROP) :])
if len(rb_groups) > curr_group:
removed = [btn for btn in rb_groups[curr_group] if btn not in buttons]
if removed:
device.clear_radio_buttons(removed)
if buttons:
device.set_radio_buttons(buttons)
elif prop_name.startswith(TOGGLE_PROP):
button_name = prop_name[len(TOGGLE_PROP) :]
for button in device.groups:
if device.groups[button].name == button_name:
device.set_toggle_mode(button, int(value))
else:
device.properties[prop_name].new_value = value
def _get_property(prop):
"""Return a property data row.""" """Return a property data row."""
value, modified = _get_usable_value(prop) value = get_usable_value(prop)
modified = value == prop.new_value
if prop.value_type in [ToggleMode, RelayMode] or prop.name == RAMP_RATE_IN_SEC:
value = str(value).lower()
prop_dict = {"name": prop.name, "value": value, "modified": modified} prop_dict = {"name": prop.name, "value": value, "modified": modified}
if isinstance(prop.value, bool): return prop_dict
schema = _bool_schema(prop.name)
def update_property(device, prop_name, value):
"""Update the value of a device property."""
prop = device.configuration[prop_name]
if prop.value_type == ToggleMode:
toggle_mode = getattr(ToggleMode, value.upper())
prop.new_value = toggle_mode
elif prop.value_type == RelayMode:
relay_mode = getattr(RelayMode, value.upper())
prop.new_value = relay_mode
else: else:
schema = _byte_schema(prop.name) prop.new_value = value
return prop_dict, {"name": prop.name, **schema}
def _get_toggle_properties(device):
"""Generate the mask properties for a KPL device."""
props = []
schema = {}
toggle_prop = device.properties[NON_TOGGLE_MASK]
toggle_on_prop = device.properties[NON_TOGGLE_ON_OFF_MASK]
for button in device.groups:
name = f"{TOGGLE_PROP}{device.groups[button].name}"
value, modified = _toggle_button_value(toggle_prop, toggle_on_prop, button)
props.append({"name": name, "value": value, "modified": modified})
toggle_schema = vol.Schema({vol.Required(name): vol.In(TOGGLE_MODES_SCHEMA)})
toggle_schema_dict = voluptuous_serialize.convert(
toggle_schema, custom_serializer=cv.custom_serializer
)
schema[name] = toggle_schema_dict[0]
return props, schema
def _toggle_button_value(non_toggle_prop, toggle_on_prop, button):
"""Determine the toggle value of a button."""
toggle_mask, toggle_modified = _get_usable_value(non_toggle_prop)
toggle_on_mask, toggle_on_modified = _get_usable_value(toggle_on_prop)
bit = button - 1
if not toggle_mask & 1 << bit:
value = 0
else:
if toggle_on_mask & 1 << bit:
value = 1
else:
value = 2
modified = False
if toggle_modified:
curr_bit = non_toggle_prop.value & 1 << bit
new_bit = non_toggle_prop.new_value & 1 << bit
modified = not curr_bit == new_bit
if not modified and value != 0 and toggle_on_modified:
curr_bit = toggle_on_prop.value & 1 << bit
new_bit = toggle_on_prop.new_value & 1 << bit
modified = not curr_bit == new_bit
return value, modified
def _get_radio_button_properties(device):
"""Return the values and schema to set KPL buttons as radio buttons."""
rb_groups = _calc_radio_button_groups(device)
props = []
schema = {}
index = 0
remaining_buttons = []
buttons_in_groups = list(chain.from_iterable(rb_groups))
# Identify buttons not belonging to any group
for button in device.groups:
if button not in buttons_in_groups:
remaining_buttons.append(button)
for rb_group in rb_groups:
name = f"{RADIO_BUTTON_GROUP_PROP}{index}"
button_1 = rb_group[0]
button_str = f"_{button_1}" if button_1 != 1 else ""
on_mask = device.properties[f"{ON_MASK}{button_str}"]
off_mask = device.properties[f"{OFF_MASK}{button_str}"]
modified = on_mask.is_dirty or off_mask.is_dirty
props.append(
{
"name": name,
"modified": modified,
"value": rb_group,
}
)
options = {
button: device.groups[button].name
for button in chain.from_iterable([rb_group, remaining_buttons])
}
rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)})
rb_schema_dict = voluptuous_serialize.convert(
rb_schema, custom_serializer=cv.custom_serializer
)
schema[name] = rb_schema_dict[0]
index += 1
if len(remaining_buttons) > 1:
name = f"{RADIO_BUTTON_GROUP_PROP}{index}"
props.append(
{
"name": name,
"modified": False,
"value": [],
}
)
options = {button: device.groups[button].name for button in remaining_buttons}
rb_schema = vol.Schema({vol.Optional(name): cv.multi_select(options)})
rb_schema_dict = voluptuous_serialize.convert(
rb_schema, custom_serializer=cv.custom_serializer
)
schema[name] = rb_schema_dict[0]
return props, schema
def _calc_radio_button_groups(device):
"""Return existing radio button groups."""
rb_groups = []
for button in device.groups:
if button not in list(chain.from_iterable(rb_groups)):
button_str = "" if button == 1 else f"_{button}"
on_mask, _ = _get_usable_value(device.properties[f"{ON_MASK}{button_str}"])
if on_mask != 0:
rb_group = [button]
for bit in list(range(0, button - 1)) + list(range(button, 8)):
if on_mask & 1 << bit:
rb_group.append(bit + 1)
if len(rb_group) > 1:
rb_groups.append(rb_group)
return rb_groups
def _get_ramp_rate_property(prop):
"""Return the value and schema of a ramp rate property."""
rr_prop, _ = _get_property(prop)
rr_prop["value"] = ramp_rate_to_seconds(rr_prop["value"])
return rr_prop, _ramp_rate_schema(prop.name)
def _get_usable_value(prop):
"""Return the current or the modified value of a property."""
value = prop.value if prop.new_value is None else prop.new_value
return value, prop.is_dirty
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {
vol.Required(TYPE): "insteon/properties/get", vol.Required(TYPE): "insteon/properties/get",
vol.Required(DEVICE_ADDRESS): str, vol.Required(DEVICE_ADDRESS): str,
vol.Required(SHOW_ADVANCED): bool,
} }
) )
@websocket_api.require_admin @websocket_api.require_admin
@ -299,7 +167,7 @@ async def websocket_get_properties(
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
return return
properties, schema = get_properties(device) properties, schema = get_properties(device, msg[SHOW_ADVANCED])
connection.send_result(msg[ID], {"properties": properties, "schema": schema}) connection.send_result(msg[ID], {"properties": properties, "schema": schema})
@ -324,7 +192,7 @@ async def websocket_change_properties_record(
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
return return
set_property(device, msg[PROPERTY_NAME], msg[PROPERTY_VALUE]) update_property(device, msg[PROPERTY_NAME], msg[PROPERTY_VALUE])
connection.send_result(msg[ID]) connection.send_result(msg[ID])
@ -346,10 +214,9 @@ async def websocket_write_properties(
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
return return
result1 = await device.async_write_op_flags() result = await device.async_write_config()
result2 = await device.async_write_ext_properties()
await devices.async_save(workdir=hass.config.config_dir) await devices.async_save(workdir=hass.config.config_dir)
if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: if result != ResponseStatus.SUCCESS:
connection.send_message( connection.send_message(
websocket_api.error_message( websocket_api.error_message(
msg[ID], "write_failed", "properties not written to device" msg[ID], "write_failed", "properties not written to device"
@ -377,10 +244,9 @@ async def websocket_load_properties(
notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND) notify_device_not_found(connection, msg, INSTEON_DEVICE_NOT_FOUND)
return return
result1 = await device.async_read_op_flags() result, _ = await device.async_read_config(read_aldb=False)
result2 = await device.async_read_ext_properties()
await devices.async_save(workdir=hass.config.config_dir) await devices.async_save(workdir=hass.config.config_dir)
if result1 != ResponseStatus.SUCCESS or result2 != ResponseStatus.SUCCESS: if result != ResponseStatus.SUCCESS:
connection.send_message( connection.send_message(
websocket_api.error_message( websocket_api.error_message(
msg[ID], "load_failed", "properties not loaded from device" msg[ID], "load_failed", "properties not loaded from device"

View File

@ -1,8 +1,8 @@
"""Support for Insteon thermostat.""" """Support for Insteon thermostat."""
from __future__ import annotations from __future__ import annotations
from pyinsteon.config import CELSIUS
from pyinsteon.constants import ThermostatMode from pyinsteon.constants import ThermostatMode
from pyinsteon.operating_flag import CELSIUS
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (

View File

@ -70,6 +70,7 @@ CONF_DIM_STEPS = "dim_steps"
CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" CONF_X10_ALL_UNITS_OFF = "x10_all_units_off"
CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on"
CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off"
CONF_DEV_PATH = "dev_path"
PORT_HUB_V1 = 9761 PORT_HUB_V1 = 9761
PORT_HUB_V2 = 25105 PORT_HUB_V2 = 25105
@ -172,5 +173,6 @@ PROPERTY_NAME = "name"
PROPERTY_VALUE = "value" PROPERTY_VALUE = "value"
HA_DEVICE_NOT_FOUND = "ha_device_not_found" HA_DEVICE_NOT_FOUND = "ha_device_not_found"
INSTEON_DEVICE_NOT_FOUND = "insteon_device_not_found" INSTEON_DEVICE_NOT_FOUND = "insteon_device_not_found"
MULTIPLE = "multiple"
INSTEON_ADDR_REGEX = re.compile(r"([A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2})$") INSTEON_ADDR_REGEX = re.compile(r"([A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2}\.?[A-Fa-f0-9]{2})$")

View File

@ -85,7 +85,7 @@ class InsteonEntity(Entity):
"""Return device information.""" """Return device information."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, str(self._insteon_device.address))}, identifiers={(DOMAIN, str(self._insteon_device.address))},
manufacturer="Smart Home", manufacturer="SmartLabs, Inc",
model=f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})", model=f"{self._insteon_device.model} ({self._insteon_device.cat!r}, 0x{self._insteon_device.subcat:02x})",
name=f"{self._insteon_device.description} {self._insteon_device.address}", name=f"{self._insteon_device.description} {self._insteon_device.address}",
sw_version=f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}", sw_version=f"{self._insteon_device.firmware:02x} Engine Version: {self._insteon_device.engine_version}",

View File

@ -1,5 +1,5 @@
"""Support for Insteon lights via PowerLinc Modem.""" """Support for Insteon lights via PowerLinc Modem."""
from pyinsteon.extended_property import ON_LEVEL from pyinsteon.config import ON_LEVEL
from homeassistant.components.light import ( from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,

View File

@ -2,13 +2,17 @@
"domain": "insteon", "domain": "insteon",
"name": "Insteon", "name": "Insteon",
"documentation": "https://www.home-assistant.io/integrations/insteon", "documentation": "https://www.home-assistant.io/integrations/insteon",
"requirements": ["pyinsteon==1.0.13"], "dependencies": ["http", "websocket_api"],
"requirements": [
"pyinsteon==1.1.0b1",
"insteon-frontend-home-assistant==0.1.0"
],
"codeowners": ["@teharris1"], "codeowners": ["@teharris1"],
"dhcp": [{ "macaddress": "000EF3*" }, { "registered_devices": true }], "dhcp": [{ "macaddress": "000EF3*" }, { "registered_devices": true }],
"config_flow": true, "config_flow": true,
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["pyinsteon", "pypubsub"], "loggers": ["pyinsteon", "pypubsub"],
"after_dependencies": ["usb"], "after_dependencies": ["panel_custom", "usb"],
"usb": [ "usb": [
{ {
"vid": "10BF" "vid": "10BF"

View File

@ -22,6 +22,7 @@ import homeassistant.helpers.config_validation as cv
from .const import ( from .const import (
CONF_CAT, CONF_CAT,
CONF_DEV_PATH,
CONF_DIM_STEPS, CONF_DIM_STEPS,
CONF_FIRMWARE, CONF_FIRMWARE,
CONF_HOUSECODE, CONF_HOUSECODE,
@ -121,6 +122,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_X10): vol.All( vol.Optional(CONF_X10): vol.All(
cv.ensure_list_csv, [CONF_X10_SCHEMA] cv.ensure_list_csv, [CONF_X10_SCHEMA]
), ),
vol.Optional(CONF_DEV_PATH): cv.string,
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
required=True, required=True,

View File

@ -4,7 +4,7 @@ import logging
from pyinsteon import devices from pyinsteon import devices
from pyinsteon.address import Address from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus from pyinsteon.constants import ALDBStatus, DeviceAction
from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT
from pyinsteon.managers.link_manager import ( from pyinsteon.managers.link_manager import (
async_enter_linking_mode, async_enter_linking_mode,
@ -137,8 +137,9 @@ def register_new_device_callback(hass):
"""Register callback for new Insteon device.""" """Register callback for new Insteon device."""
@callback @callback
def async_new_insteon_device(address=None): def async_new_insteon_device(address, action: DeviceAction):
"""Detect device from transport to be delegated to platform.""" """Detect device from transport to be delegated to platform."""
if action == DeviceAction.ADDED:
hass.async_create_task(async_create_new_entities(address)) hass.async_create_task(async_create_new_entities(address))
async def async_create_new_entities(address): async def async_create_new_entities(address):

View File

@ -881,6 +881,9 @@ influxdb-client==1.24.0
# homeassistant.components.influxdb # homeassistant.components.influxdb
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.1.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==1.0.2 intellifire4py==1.0.2
@ -1544,7 +1547,7 @@ pyialarm==1.9.0
pyicloud==1.0.0 pyicloud==1.0.0
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.0.13 pyinsteon==1.1.0b1
# homeassistant.components.intesishome # homeassistant.components.intesishome
pyintesishome==1.7.6 pyintesishome==1.7.6

View File

@ -618,6 +618,9 @@ influxdb-client==1.24.0
# homeassistant.components.influxdb # homeassistant.components.influxdb
influxdb==5.3.1 influxdb==5.3.1
# homeassistant.components.insteon
insteon-frontend-home-assistant==0.1.0
# homeassistant.components.intellifire # homeassistant.components.intellifire
intellifire4py==1.0.2 intellifire4py==1.0.2
@ -1026,7 +1029,7 @@ pyialarm==1.9.0
pyicloud==1.0.0 pyicloud==1.0.0
# homeassistant.components.insteon # homeassistant.components.insteon
pyinsteon==1.0.13 pyinsteon==1.1.0b1
# homeassistant.components.ipma # homeassistant.components.ipma
pyipma==2.0.5 pyipma==2.0.5

View File

@ -0,0 +1,18 @@
{
"operating_flags": {
"program_lock_on": false,
"blink_on_tx_on": true,
"relay_on_sense_on": false,
"momentary_on": true,
"momentary_on_off_trigger": false,
"x10_off": true,
"sense_sends_off": true,
"momentary_follow_sense": false
},
"properties": {
"prescaler": 1,
"delay": 50,
"x10_house": 32,
"x10_unit": 0
}
}

View File

@ -1,15 +1,20 @@
"""Mock devices object to test Insteon.""" """Mock devices object to test Insteon."""
import asyncio
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from pyinsteon.address import Address from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus, ResponseStatus from pyinsteon.constants import ALDBStatus, ResponseStatus
from pyinsteon.device_types import ( from pyinsteon.device_types import (
DimmableLightingControl_KeypadLinc_8, DimmableLightingControl_KeypadLinc_8,
GeneralController, GeneralController_RemoteLinc,
Hub, Hub,
SensorsActuators_IOLink,
SwitchedLightingControl_SwitchLinc, SwitchedLightingControl_SwitchLinc,
) )
from pyinsteon.managers.saved_devices_manager import dict_to_aldb_record from pyinsteon.managers.saved_devices_manager import dict_to_aldb_record
from pyinsteon.topics import DEVICE_LIST_CHANGED
from pyinsteon.utils import subscribe_topic
class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): class MockSwitchLinc(SwitchedLightingControl_SwitchLinc):
@ -31,7 +36,10 @@ class MockDevices:
self._connected = connected self._connected = connected
self.async_save = AsyncMock() self.async_save = AsyncMock()
self.add_x10_device = MagicMock() self.add_x10_device = MagicMock()
self.async_read_config = AsyncMock()
self.set_id = MagicMock() self.set_id = MagicMock()
self.async_add_device_called_with = {}
self.async_cancel_all_linking = AsyncMock()
def __getitem__(self, address): def __getitem__(self, address):
"""Return a a device from the device address.""" """Return a a device from the device address."""
@ -56,18 +64,24 @@ class MockDevices:
addr1 = Address("11.11.11") addr1 = Address("11.11.11")
addr2 = Address("22.22.22") addr2 = Address("22.22.22")
addr3 = Address("33.33.33") addr3 = Address("33.33.33")
addr4 = Address("44.44.44")
self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0") self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0")
self._devices[addr1] = MockSwitchLinc( self._devices[addr1] = MockSwitchLinc(
addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1" addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1"
) )
self._devices[addr2] = GeneralController( self._devices[addr2] = GeneralController_RemoteLinc(
addr2, 0x00, 0x00, 0x00, "Device 22.22.22", "2" addr2, 0x00, 0x00, 0x00, "Device 22.22.22", "2"
) )
self._devices[addr3] = DimmableLightingControl_KeypadLinc_8( self._devices[addr3] = DimmableLightingControl_KeypadLinc_8(
addr3, 0x02, 0x00, 0x00, "Device 33.33.33", "3" addr3, 0x02, 0x00, 0x00, "Device 33.33.33", "3"
) )
self._devices[addr4] = SensorsActuators_IOLink(
addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4"
)
for device in [self._devices[addr] for addr in [addr1, addr2, addr3]]: for device in [
self._devices[addr] for addr in [addr1, addr2, addr3, addr4]
]:
device.async_read_config = AsyncMock() device.async_read_config = AsyncMock()
device.aldb.async_write = AsyncMock() device.aldb.async_write = AsyncMock()
device.aldb.async_load = AsyncMock() device.aldb.async_load = AsyncMock()
@ -85,7 +99,7 @@ class MockDevices:
return_value=ResponseStatus.SUCCESS return_value=ResponseStatus.SUCCESS
) )
for device in [self._devices[addr] for addr in [addr2, addr3]]: for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]:
device.async_status = AsyncMock() device.async_status = AsyncMock()
self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError) self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError)
self._devices[addr0].aldb.async_load = AsyncMock() self._devices[addr0].aldb.async_load = AsyncMock()
@ -104,6 +118,7 @@ class MockDevices:
) )
self.modem = self._devices[addr0] self.modem = self._devices[addr0]
self.modem.async_read_config = AsyncMock()
def fill_aldb(self, address, records): def fill_aldb(self, address, records):
"""Fill the All-Link Database for a device.""" """Fill the All-Link Database for a device."""
@ -126,3 +141,18 @@ class MockDevices:
value = properties[flag] value = properties[flag]
if device.properties.get(flag): if device.properties.get(flag):
device.properties[flag].load(value) device.properties[flag].load(value)
async def async_add_device(self, address=None, multiple=False):
"""Mock the async_add_device method."""
self.async_add_device_called_with = {"address": address, "multiple": multiple}
if multiple:
yield "aa.bb.cc"
await asyncio.sleep(0.01)
yield "bb.cc.dd"
if address:
yield address
await asyncio.sleep(0.01)
def subscribe(self, listener):
"""Mock the subscribe function."""
subscribe_topic(listener, DEVICE_LIST_CHANGED)

View File

@ -1,6 +1,11 @@
"""Test the device level APIs.""" """Test the device level APIs."""
import asyncio
from unittest.mock import patch from unittest.mock import patch
from pyinsteon.constants import DeviceAction
from pyinsteon.topics import DEVICE_LIST_CHANGED
from pyinsteon.utils import publish_topic
from homeassistant.components import insteon from homeassistant.components import insteon
from homeassistant.components.insteon.api import async_load_api from homeassistant.components.insteon.api import async_load_api
from homeassistant.components.insteon.api.device import ( from homeassistant.components.insteon.api.device import (
@ -11,7 +16,7 @@ from homeassistant.components.insteon.api.device import (
TYPE, TYPE,
async_device_name, async_device_name,
) )
from homeassistant.components.insteon.const import DOMAIN from homeassistant.components.insteon.const import DOMAIN, MULTIPLE
from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.device_registry import async_get_registry
from .const import MOCK_USER_INPUT_PLM from .const import MOCK_USER_INPUT_PLM
@ -137,3 +142,47 @@ async def test_get_ha_device_name(hass, hass_ws_client):
# Test no HA or Insteon device # Test no HA or Insteon device
name = await async_device_name(device_reg, "BB.BB.BB") name = await async_device_name(device_reg, "BB.BB.BB")
assert name == "" assert name == ""
async def test_add_device_api(hass, hass_ws_client):
"""Test adding an Insteon device."""
ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client)
with patch.object(insteon.api.device, "devices", devices):
await ws_client.send_json({ID: 2, TYPE: "insteon/device/add", MULTIPLE: True})
await asyncio.sleep(0.01)
assert devices.async_add_device_called_with.get("address") is None
assert devices.async_add_device_called_with["multiple"] is True
msg = await ws_client.receive_json()
assert msg["event"]["type"] == "device_added"
assert msg["event"]["address"] == "aa.bb.cc"
msg = await ws_client.receive_json()
assert msg["event"]["type"] == "device_added"
assert msg["event"]["address"] == "bb.cc.dd"
publish_topic(
DEVICE_LIST_CHANGED,
address=None,
action=DeviceAction.COMPLETED,
)
msg = await ws_client.receive_json()
assert msg["event"]["type"] == "linking_stopped"
async def test_cancel_add_device(hass, hass_ws_client):
"""Test cancelling adding of a new device."""
ws_client, devices, _, _ = await _async_setup(hass, hass_ws_client)
with patch.object(insteon.api.aldb, "devices", devices):
await ws_client.send_json(
{
ID: 2,
TYPE: "insteon/device/add/cancel",
}
)
msg = await ws_client.receive_json()
assert msg["success"]

View File

@ -1,8 +1,11 @@
"""Test the Insteon properties APIs.""" """Test the Insteon properties APIs."""
import json import json
from unittest.mock import patch from unittest.mock import AsyncMock, patch
from pyinsteon.config import MOMENTARY_DELAY, RELAY_MODE, TOGGLE_BUTTON
from pyinsteon.config.extended_property import ExtendedProperty
from pyinsteon.constants import RelayMode, ToggleMode
import pytest import pytest
from homeassistant.components import insteon from homeassistant.components import insteon
@ -11,19 +14,12 @@ from homeassistant.components.insteon.api.device import INSTEON_DEVICE_NOT_FOUND
from homeassistant.components.insteon.api.properties import ( from homeassistant.components.insteon.api.properties import (
DEVICE_ADDRESS, DEVICE_ADDRESS,
ID, ID,
NON_TOGGLE_MASK,
NON_TOGGLE_OFF_MODE,
NON_TOGGLE_ON_MODE,
NON_TOGGLE_ON_OFF_MASK,
PROPERTY_NAME, PROPERTY_NAME,
PROPERTY_VALUE, PROPERTY_VALUE,
RADIO_BUTTON_GROUP_PROP, RADIO_BUTTON_GROUPS,
TOGGLE_MODES, RAMP_RATE_IN_SEC,
TOGGLE_ON_OFF_MODE, SHOW_ADVANCED,
TOGGLE_PROP,
TYPE, TYPE,
_get_radio_button_properties,
_get_toggle_properties,
) )
from .mock_devices import MockDevices from .mock_devices import MockDevices
@ -31,43 +27,172 @@ from .mock_devices import MockDevices
from tests.common import load_fixture from tests.common import load_fixture
@pytest.fixture(name="properties_data", scope="session") @pytest.fixture(name="kpl_properties_data", scope="session")
def aldb_data_fixture(): def kpl_properties_data_fixture():
"""Load the controller state fixture data.""" """Load the controller state fixture data."""
return json.loads(load_fixture("insteon/kpl_properties.json")) return json.loads(load_fixture("insteon/kpl_properties.json"))
async def _setup(hass, hass_ws_client, properties_data): @pytest.fixture(name="iolinc_properties_data", scope="session")
def iolinc_properties_data_fixture():
"""Load the controller state fixture data."""
return json.loads(load_fixture("insteon/iolinc_properties.json"))
async def _setup(hass, hass_ws_client, address, properties_data):
"""Set up tests.""" """Set up tests."""
ws_client = await hass_ws_client(hass) ws_client = await hass_ws_client(hass)
devices = MockDevices() devices = MockDevices()
await devices.async_load() await devices.async_load()
devices.fill_properties("33.33.33", properties_data) devices.fill_properties(address, properties_data)
async_load_api(hass) async_load_api(hass)
return ws_client, devices return ws_client, devices
async def test_get_properties(hass, hass_ws_client, properties_data): async def test_get_properties(
hass, hass_ws_client, kpl_properties_data, iolinc_properties_data
):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{ID: 2, TYPE: "insteon/properties/get", DEVICE_ADDRESS: "33.33.33"}
) )
msg = await ws_client.receive_json() devices.fill_properties("44.44.44", iolinc_properties_data)
assert msg["success"]
assert len(msg["result"]["properties"]) == 54
async def test_change_operating_flag(hass, hass_ws_client, properties_data):
"""Test changing an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data)
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 2, ID: 2,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "33.33.33",
SHOW_ADVANCED: False,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 18
await ws_client.send_json(
{
ID: 3,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: False,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 6
await ws_client.send_json(
{
ID: 4,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "33.33.33",
SHOW_ADVANCED: True,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 69
await ws_client.send_json(
{
ID: 5,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: True,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 14
async def test_get_read_only_properties(hass, hass_ws_client, iolinc_properties_data):
"""Test getting an Insteon device's properties."""
mock_read_only = ExtendedProperty(
"44.44.44", "mock_read_only", bool, is_read_only=True
)
mock_read_only.load(False)
ws_client, devices = await _setup(
hass, hass_ws_client, "44.44.44", iolinc_properties_data
)
device = devices["44.44.44"]
device.configuration["mock_read_only"] = mock_read_only
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{
ID: 2,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: False,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 6
await ws_client.send_json(
{
ID: 3,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: True,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 15
async def test_get_unknown_properties(hass, hass_ws_client, iolinc_properties_data):
"""Test getting an Insteon device's properties."""
class UnknownType:
"""Mock unknown data type."""
mock_unknown = ExtendedProperty("44.44.44", "mock_unknown", UnknownType)
ws_client, devices = await _setup(
hass, hass_ws_client, "44.44.44", iolinc_properties_data
)
device = devices["44.44.44"]
device.configuration["mock_unknown"] = mock_unknown
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{
ID: 2,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: False,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 6
await ws_client.send_json(
{
ID: 3,
TYPE: "insteon/properties/get",
DEVICE_ADDRESS: "44.44.44",
SHOW_ADVANCED: True,
}
)
msg = await ws_client.receive_json()
assert msg["success"]
assert len(msg["result"]["properties"]) == 14
async def test_change_bool_property(hass, hass_ws_client, kpl_properties_data):
"""Test changing a bool type properties."""
ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{
ID: 3,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: "led_off", PROPERTY_NAME: "led_off",
@ -79,29 +204,33 @@ async def test_change_operating_flag(hass, hass_ws_client, properties_data):
assert devices["33.33.33"].operating_flags["led_off"].is_dirty assert devices["33.33.33"].operating_flags["led_off"].is_dirty
async def test_change_property(hass, hass_ws_client, properties_data): async def test_change_int_property(hass, hass_ws_client, kpl_properties_data):
"""Test changing an Insteon device's properties.""" """Test changing a int type properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 2, ID: 4,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: "on_mask", PROPERTY_NAME: "led_dimming",
PROPERTY_VALUE: 100, PROPERTY_VALUE: 100,
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert devices["33.33.33"].properties["on_mask"].new_value == 100 assert devices["33.33.33"].properties["led_dimming"].new_value == 100
assert devices["33.33.33"].properties["on_mask"].is_dirty assert devices["33.33.33"].properties["led_dimming"].is_dirty
async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data): async def test_change_ramp_rate_property(hass, hass_ws_client, kpl_properties_data):
"""Test changing an Insteon device's properties.""" """Test changing an Insteon device's ramp rate properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
@ -109,7 +238,7 @@ async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data):
ID: 2, ID: 2,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: "ramp_rate", PROPERTY_NAME: RAMP_RATE_IN_SEC,
PROPERTY_VALUE: 4.5, PROPERTY_VALUE: 4.5,
} }
) )
@ -119,208 +248,126 @@ async def test_change_ramp_rate_property(hass, hass_ws_client, properties_data):
assert devices["33.33.33"].properties["ramp_rate"].is_dirty assert devices["33.33.33"].properties["ramp_rate"].is_dirty
async def test_change_radio_button_group(hass, hass_ws_client, properties_data): async def test_change_radio_button_group(hass, hass_ws_client, kpl_properties_data):
"""Test changing an Insteon device's properties.""" """Test changing an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
rb_props, schema = _get_radio_button_properties(devices["33.33.33"]) hass, hass_ws_client, "33.33.33", kpl_properties_data
)
rb_groups = devices["33.33.33"].configuration[RADIO_BUTTON_GROUPS]
# Make sure the baseline is correct # Make sure the baseline is correct
assert rb_props[0]["name"] == f"{RADIO_BUTTON_GROUP_PROP}0" assert rb_groups.value[0] == [4, 5]
assert rb_props[0]["value"] == [4, 5] assert rb_groups.value[1] == [7, 8]
assert rb_props[1]["value"] == [7, 8]
assert rb_props[2]["value"] == []
assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1)
assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1)
assert devices["33.33.33"].properties["on_mask"].value == 0
assert devices["33.33.33"].properties["off_mask"].value == 0
assert not devices["33.33.33"].properties["on_mask"].is_dirty
assert not devices["33.33.33"].properties["off_mask"].is_dirty
# Add button 1 to the group # Add button 1 to the group
rb_props[0]["value"].append(1) new_groups_1 = [[1, 4, 5], [7, 8]]
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 2, ID: 2,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", PROPERTY_NAME: RADIO_BUTTON_GROUPS,
PROPERTY_VALUE: rb_props[0]["value"], PROPERTY_VALUE: new_groups_1,
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert rb_groups.new_value[0] == [1, 4, 5]
assert rb_groups.new_value[1] == [7, 8]
new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) new_groups_2 = [[1, 4], [7, 8]]
assert 1 in new_rb_props[0]["value"]
assert 4 in new_rb_props[0]["value"]
assert 5 in new_rb_props[0]["value"]
assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1)
assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1)
assert devices["33.33.33"].properties["on_mask"].new_value == 0x18
assert devices["33.33.33"].properties["off_mask"].new_value == 0x18
assert devices["33.33.33"].properties["on_mask"].is_dirty
assert devices["33.33.33"].properties["off_mask"].is_dirty
# Remove button 5
rb_props[0]["value"].remove(5)
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 3, ID: 3,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}0", PROPERTY_NAME: RADIO_BUTTON_GROUPS,
PROPERTY_VALUE: rb_props[0]["value"], PROPERTY_VALUE: new_groups_2,
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert rb_groups.new_value[0] == [1, 4]
new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"]) assert rb_groups.new_value[1] == [7, 8]
assert 1 in new_rb_props[0]["value"]
assert 4 in new_rb_props[0]["value"]
assert 5 not in new_rb_props[0]["value"]
assert schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1)
assert schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1)
assert devices["33.33.33"].properties["on_mask"].new_value == 0x08
assert devices["33.33.33"].properties["off_mask"].new_value == 0x08
assert devices["33.33.33"].properties["on_mask"].is_dirty
assert devices["33.33.33"].properties["off_mask"].is_dirty
# Remove button group 1
rb_props[1]["value"] = []
await ws_client.send_json(
{
ID: 5,
TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}1",
PROPERTY_VALUE: rb_props[1]["value"],
}
)
msg = await ws_client.receive_json()
assert msg["success"]
new_rb_props, _ = _get_radio_button_properties(devices["33.33.33"])
assert len(new_rb_props) == 2
assert new_rb_props[0]["value"] == [1, 4]
assert new_rb_props[1]["value"] == []
async def test_create_radio_button_group(hass, hass_ws_client, properties_data): async def test_change_toggle_property(hass, hass_ws_client, kpl_properties_data):
"""Test changing an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data)
rb_props, _ = _get_radio_button_properties(devices["33.33.33"])
# Make sure the baseline is correct
assert len(rb_props) == 3
rb_props[0]["value"].append("1")
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json(
{
ID: 2,
TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: f"{RADIO_BUTTON_GROUP_PROP}2",
PROPERTY_VALUE: ["1", "3"],
}
)
msg = await ws_client.receive_json()
assert msg["success"]
new_rb_props, new_schema = _get_radio_button_properties(devices["33.33.33"])
assert len(new_rb_props) == 4
assert 1 in new_rb_props[0]["value"]
assert new_schema[f"{RADIO_BUTTON_GROUP_PROP}0"]["options"].get(1)
assert not new_schema[f"{RADIO_BUTTON_GROUP_PROP}1"]["options"].get(1)
assert devices["33.33.33"].properties["on_mask"].new_value == 4
assert devices["33.33.33"].properties["off_mask"].new_value == 4
assert devices["33.33.33"].properties["on_mask"].is_dirty
assert devices["33.33.33"].properties["off_mask"].is_dirty
async def test_change_toggle_property(hass, hass_ws_client, properties_data):
"""Update a button's toggle mode.""" """Update a button's toggle mode."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
device = devices["33.33.33"] device = devices["33.33.33"]
toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) prop_name = f"{TOGGLE_BUTTON}_c"
toggle_prop = device.configuration[prop_name]
# Make sure the baseline is correct assert toggle_prop.value == ToggleMode.TOGGLE
assert toggle_props[0]["name"] == f"{TOGGLE_PROP}{device.groups[1].name}"
assert toggle_props[0]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE]
assert toggle_props[1]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE]
assert device.properties[NON_TOGGLE_MASK].value == 2
assert device.properties[NON_TOGGLE_ON_OFF_MASK].value == 2
assert not device.properties[NON_TOGGLE_MASK].is_dirty
assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 2, ID: 2,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "33.33.33",
PROPERTY_NAME: toggle_props[0]["name"], PROPERTY_NAME: prop_name,
PROPERTY_VALUE: 1, PROPERTY_VALUE: str(ToggleMode.ON_ONLY).lower(),
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert toggle_prop.new_value == ToggleMode.ON_ONLY
new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"])
assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_ON_MODE]
assert device.properties[NON_TOGGLE_MASK].new_value == 3
assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 3
assert device.properties[NON_TOGGLE_MASK].is_dirty
assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty
async def test_change_relay_mode(hass, hass_ws_client, iolinc_properties_data):
"""Update a device's relay mode."""
ws_client, devices = await _setup(
hass, hass_ws_client, "44.44.44", iolinc_properties_data
)
device = devices["44.44.44"]
relay_prop = device.configuration[RELAY_MODE]
assert relay_prop.value == RelayMode.MOMENTARY_A
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 3, ID: 2,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "44.44.44",
PROPERTY_NAME: toggle_props[0]["name"], PROPERTY_NAME: RELAY_MODE,
PROPERTY_VALUE: 2, PROPERTY_VALUE: str(RelayMode.LATCHING).lower(),
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert relay_prop.new_value == RelayMode.LATCHING
new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"])
assert new_toggle_props[0]["value"] == TOGGLE_MODES[NON_TOGGLE_OFF_MODE]
assert device.properties[NON_TOGGLE_MASK].new_value == 3
assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value is None
assert device.properties[NON_TOGGLE_MASK].is_dirty
assert not device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty
async def test_change_float_property(hass, hass_ws_client, iolinc_properties_data):
"""Update a float type property."""
ws_client, devices = await _setup(
hass, hass_ws_client, "44.44.44", iolinc_properties_data
)
device = devices["44.44.44"]
delay_prop = device.configuration[MOMENTARY_DELAY]
delay_prop.load(0)
with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ {
ID: 4, ID: 2,
TYPE: "insteon/properties/change", TYPE: "insteon/properties/change",
DEVICE_ADDRESS: "33.33.33", DEVICE_ADDRESS: "44.44.44",
PROPERTY_NAME: toggle_props[1]["name"], PROPERTY_NAME: MOMENTARY_DELAY,
PROPERTY_VALUE: 0, PROPERTY_VALUE: 1.8,
} }
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
new_toggle_props, _ = _get_toggle_properties(devices["33.33.33"]) assert delay_prop.new_value == 1.8
assert new_toggle_props[1]["value"] == TOGGLE_MODES[TOGGLE_ON_OFF_MODE]
assert device.properties[NON_TOGGLE_MASK].new_value == 1
assert device.properties[NON_TOGGLE_ON_OFF_MASK].new_value == 0
assert device.properties[NON_TOGGLE_MASK].is_dirty
assert device.properties[NON_TOGGLE_ON_OFF_MASK].is_dirty
async def test_write_properties(hass, hass_ws_client, properties_data): async def test_write_properties(hass, hass_ws_client, kpl_properties_data):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
@ -332,9 +379,11 @@ async def test_write_properties(hass, hass_ws_client, properties_data):
assert devices["33.33.33"].async_write_ext_properties.call_count == 1 assert devices["33.33.33"].async_write_ext_properties.call_count == 1
async def test_write_properties_failure(hass, hass_ws_client, properties_data): async def test_write_properties_failure(hass, hass_ws_client, kpl_properties_data):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
@ -345,39 +394,48 @@ async def test_write_properties_failure(hass, hass_ws_client, properties_data):
assert msg["error"]["code"] == "write_failed" assert msg["error"]["code"] == "write_failed"
async def test_load_properties(hass, hass_ws_client, properties_data): async def test_load_properties(hass, hass_ws_client, kpl_properties_data):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
device = devices["33.33.33"]
device.async_read_config = AsyncMock(return_value=(1, 1))
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"} {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"}
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert msg["success"] assert msg["success"]
assert devices["33.33.33"].async_read_op_flags.call_count == 1 assert devices["33.33.33"].async_read_config.call_count == 1
assert devices["33.33.33"].async_read_ext_properties.call_count == 1
async def test_load_properties_failure(hass, hass_ws_client, properties_data): async def test_load_properties_failure(hass, hass_ws_client, kpl_properties_data):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
device = devices["33.33.33"]
device.async_read_config = AsyncMock(return_value=(0, 0))
with patch.object(insteon.api.properties, "devices", devices): with patch.object(insteon.api.properties, "devices", devices):
await ws_client.send_json( await ws_client.send_json(
{ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "22.22.22"} {ID: 2, TYPE: "insteon/properties/load", DEVICE_ADDRESS: "33.33.33"}
) )
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert not msg["success"] assert not msg["success"]
assert msg["error"]["code"] == "load_failed" assert msg["error"]["code"] == "load_failed"
async def test_reset_properties(hass, hass_ws_client, properties_data): async def test_reset_properties(hass, hass_ws_client, kpl_properties_data):
"""Test getting an Insteon device's properties.""" """Test getting an Insteon device's properties."""
ws_client, devices = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
device = devices["33.33.33"] device = devices["33.33.33"]
device.operating_flags["led_off"].new_value = True device.configuration["led_off"].new_value = True
device.properties["on_mask"].new_value = 100 device.properties["on_mask"].new_value = 100
assert device.operating_flags["led_off"].is_dirty assert device.operating_flags["led_off"].is_dirty
assert device.properties["on_mask"].is_dirty assert device.properties["on_mask"].is_dirty
@ -391,20 +449,23 @@ async def test_reset_properties(hass, hass_ws_client, properties_data):
assert not device.properties["on_mask"].is_dirty assert not device.properties["on_mask"].is_dirty
async def test_bad_address(hass, hass_ws_client, properties_data): async def test_bad_address(hass, hass_ws_client, kpl_properties_data):
"""Test for a bad Insteon address.""" """Test for a bad Insteon address."""
ws_client, _ = await _setup(hass, hass_ws_client, properties_data) ws_client, devices = await _setup(
hass, hass_ws_client, "33.33.33", kpl_properties_data
)
ws_id = 0 ws_id = 0
for call in ["get", "write", "load", "reset"]: for call in ["get", "write", "load", "reset"]:
ws_id += 1 ws_id += 1
await ws_client.send_json( params = {
{
ID: ws_id, ID: ws_id,
TYPE: f"insteon/properties/{call}", TYPE: f"insteon/properties/{call}",
DEVICE_ADDRESS: "99.99.99", DEVICE_ADDRESS: "99.99.99",
} }
) if call == "get":
params[SHOW_ADVANCED] = False
await ws_client.send_json(params)
msg = await ws_client.receive_json() msg = await ws_client.receive_json()
assert not msg["success"] assert not msg["success"]
assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND assert msg["error"]["message"] == INSTEON_DEVICE_NOT_FOUND

View File

@ -2,10 +2,8 @@
from unittest.mock import patch from unittest.mock import patch
import voluptuous_serialize
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components import dhcp, usb from homeassistant.components import usb
from homeassistant.components.insteon.config_flow import ( from homeassistant.components.insteon.config_flow import (
HUB1, HUB1,
HUB2, HUB2,
@ -39,7 +37,6 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
import homeassistant.helpers.config_validation as cv
from .const import ( from .const import (
MOCK_HOSTNAME, MOCK_HOSTNAME,
@ -651,48 +648,3 @@ async def test_discovery_via_usb_already_setup(hass):
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed" assert result["reason"] == "single_instance_allowed"
async def test_discovery_via_dhcp_hubv1(hass):
"""Test usb flow."""
await _test_dhcp(hass, HUB1)
async def test_discovery_via_dhcp_hubv2(hass):
"""Test usb flow."""
await _test_dhcp(hass, HUB2)
async def _test_dhcp(hass, modem_type):
"""Test the dhcp discovery for a moddem type."""
discovery_info = dhcp.DhcpServiceInfo(
ip="11.22.33.44", hostname="", macaddress="00:0e:f3:aa:bb:cc"
)
result = await hass.config_entries.flow.async_init(
"insteon",
context={"source": config_entries.SOURCE_DHCP},
data=discovery_info,
)
await hass.async_block_till_done()
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
with patch("homeassistant.components.insteon.config_flow.async_connect"), patch(
"homeassistant.components.insteon.async_setup_entry", return_value=True
):
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input={"modem_type": modem_type}
)
await hass.async_block_till_done()
assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM
assert result["step_id"] == "user"
schema = voluptuous_serialize.convert(
result2["data_schema"],
custom_serializer=cv.custom_serializer,
)
for field in schema:
if field["name"] == "host":
assert field.get("default") == "11.22.33.44"
break

View File

@ -7,6 +7,7 @@ from pyinsteon.address import Address
from homeassistant.components import insteon from homeassistant.components import insteon
from homeassistant.components.insteon.const import ( from homeassistant.components.insteon.const import (
CONF_CAT, CONF_CAT,
CONF_DEV_PATH,
CONF_OVERRIDE, CONF_OVERRIDE,
CONF_SUBCAT, CONF_SUBCAT,
CONF_X10, CONF_X10,
@ -222,3 +223,24 @@ async def test_setup_entry_failed_connection(hass: HomeAssistant, caplog):
{}, {},
) )
assert "Could not connect to Insteon modem" in caplog.text assert "Could not connect to Insteon modem" in caplog.text
async def test_import_frontend_dev_url(hass: HomeAssistant):
"""Test importing a dev_url config entry."""
config = {}
config[DOMAIN] = {CONF_DEV_PATH: "/some/path"}
with patch.object(
insteon, "async_connect", new=mock_successful_connection
), patch.object(insteon, "close_insteon_connection"), patch.object(
insteon, "devices", new=MockDevices()
), patch(
PATCH_CONNECTION, new=mock_successful_connection
):
assert await async_setup_component(
hass,
insteon.DOMAIN,
config,
)
await hass.async_block_till_done()
await asyncio.sleep(0.01)