Add Insteon lock and load controller devices (#75632)

pull/76119/head
Tom Harris 2022-07-27 16:06:33 -04:00 committed by Franck Nijhof
parent 3b8650d053
commit 70731c0bc7
No known key found for this signature in database
GPG Key ID: D62583BA8AB11CA3
8 changed files with 184 additions and 7 deletions

View File

@ -44,6 +44,7 @@ INSTEON_PLATFORMS = [
Platform.COVER,
Platform.FAN,
Platform.LIGHT,
Platform.LOCK,
Platform.SWITCH,
]

View File

@ -1,5 +1,6 @@
"""Utility methods for the Insteon platform."""
from pyinsteon.device_types import (
AccessControl_Morningstar,
ClimateControl_Thermostat,
ClimateControl_WirelessThermostat,
DimmableLightingControl,
@ -12,6 +13,7 @@ from pyinsteon.device_types import (
DimmableLightingControl_OutletLinc,
DimmableLightingControl_SwitchLinc,
DimmableLightingControl_ToggleLinc,
EnergyManagement_LoadController,
GeneralController_ControlLinc,
GeneralController_MiniRemote_4,
GeneralController_MiniRemote_8,
@ -44,11 +46,13 @@ from homeassistant.components.climate import DOMAIN as CLIMATE
from homeassistant.components.cover import DOMAIN as COVER
from homeassistant.components.fan import DOMAIN as FAN
from homeassistant.components.light import DOMAIN as LIGHT
from homeassistant.components.lock import DOMAIN as LOCK
from homeassistant.components.switch import DOMAIN as SWITCH
from .const import ON_OFF_EVENTS
DEVICE_PLATFORM = {
AccessControl_Morningstar: {LOCK: [1]},
DimmableLightingControl: {LIGHT: [1], ON_OFF_EVENTS: [1]},
DimmableLightingControl_DinRail: {LIGHT: [1], ON_OFF_EVENTS: [1]},
DimmableLightingControl_FanLinc: {LIGHT: [1], FAN: [2], ON_OFF_EVENTS: [1, 2]},
@ -67,6 +71,7 @@ DEVICE_PLATFORM = {
DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]},
EnergyManagement_LoadController: {SWITCH: [1], BINARY_SENSOR: [2]},
GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]},
GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)},
GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)},

View File

@ -0,0 +1,49 @@
"""Support for INSTEON locks."""
from typing import Any
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import SIGNAL_ADD_ENTITIES
from .insteon_entity import InsteonEntity
from .utils import async_add_insteon_entities
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Insteon locks from a config entry."""
@callback
def async_add_insteon_lock_entities(discovery_info=None):
"""Add the Insteon entities for the platform."""
async_add_insteon_entities(
hass, LOCK_DOMAIN, InsteonLockEntity, async_add_entities, discovery_info
)
signal = f"{SIGNAL_ADD_ENTITIES}_{LOCK_DOMAIN}"
async_dispatcher_connect(hass, signal, async_add_insteon_lock_entities)
async_add_insteon_lock_entities()
class InsteonLockEntity(InsteonEntity, LockEntity):
"""A Class for an Insteon lock entity."""
@property
def is_locked(self) -> bool:
"""Return the boolean response if the node is on."""
return bool(self._insteon_device_group.value)
async def async_lock(self, **kwargs: Any) -> None:
"""Lock the device."""
await self._insteon_device.async_lock()
async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the device."""
await self._insteon_device.async_unlock()

View File

@ -4,7 +4,7 @@
"documentation": "https://www.home-assistant.io/integrations/insteon",
"dependencies": ["http", "websocket_api"],
"requirements": [
"pyinsteon==1.1.3",
"pyinsteon==1.2.0",
"insteon-frontend-home-assistant==0.2.0"
],
"codeowners": ["@teharris1"],

View File

@ -1569,7 +1569,7 @@ pyialarm==2.2.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.1.3
pyinsteon==1.2.0
# homeassistant.components.intesishome
pyintesishome==1.8.0

View File

@ -1076,7 +1076,7 @@ pyialarm==2.2.0
pyicloud==1.0.0
# homeassistant.components.insteon
pyinsteon==1.1.3
pyinsteon==1.2.0
# homeassistant.components.ipma
pyipma==2.0.5

View File

@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
from pyinsteon.address import Address
from pyinsteon.constants import ALDBStatus, ResponseStatus
from pyinsteon.device_types import (
AccessControl_Morningstar,
DimmableLightingControl_KeypadLinc_8,
GeneralController_RemoteLinc,
Hub,
@ -59,12 +60,13 @@ class MockDevices:
async def async_load(self, *args, **kwargs):
"""Load the mock devices."""
if self._connected:
if self._connected and not self._devices:
addr0 = Address("AA.AA.AA")
addr1 = Address("11.11.11")
addr2 = Address("22.22.22")
addr3 = Address("33.33.33")
addr4 = Address("44.44.44")
addr5 = Address("55.55.55")
self._devices[addr0] = Hub(addr0, 0x03, 0x00, 0x00, "Hub AA.AA.AA", "0")
self._devices[addr1] = MockSwitchLinc(
addr1, 0x02, 0x00, 0x00, "Device 11.11.11", "1"
@ -78,9 +80,12 @@ class MockDevices:
self._devices[addr4] = SensorsActuators_IOLink(
addr4, 0x07, 0x00, 0x00, "Device 44.44.44", "4"
)
self._devices[addr5] = AccessControl_Morningstar(
addr5, 0x0F, 0x0A, 0x00, "Device 55.55.55", "5"
)
for device in [
self._devices[addr] for addr in [addr1, addr2, addr3, addr4]
self._devices[addr] for addr in [addr1, addr2, addr3, addr4, addr5]
]:
device.async_read_config = AsyncMock()
device.aldb.async_write = AsyncMock()
@ -99,7 +104,9 @@ class MockDevices:
return_value=ResponseStatus.SUCCESS
)
for device in [self._devices[addr] for addr in [addr2, addr3, addr4]]:
for device in [
self._devices[addr] for addr in [addr2, addr3, addr4, addr5]
]:
device.async_status = AsyncMock()
self._devices[addr1].async_status = AsyncMock(side_effect=AttributeError)
self._devices[addr0].aldb.async_load = AsyncMock()
@ -117,6 +124,12 @@ class MockDevices:
return_value=ResponseStatus.FAILURE
)
self._devices[addr5].async_lock = AsyncMock(
return_value=ResponseStatus.SUCCESS
)
self._devices[addr5].async_unlock = AsyncMock(
return_value=ResponseStatus.SUCCESS
)
self.modem = self._devices[addr0]
self.modem.async_read_config = AsyncMock()
@ -155,6 +168,6 @@ class MockDevices:
yield address
await asyncio.sleep(0.01)
def subscribe(self, listener):
def subscribe(self, listener, force_strong_ref=False):
"""Mock the subscribe function."""
subscribe_topic(listener, DEVICE_LIST_CHANGED)

View File

@ -0,0 +1,109 @@
"""Tests for the Insteon lock."""
from unittest.mock import patch
import pytest
from homeassistant.components import insteon
from homeassistant.components.insteon import (
DOMAIN,
insteon_entity,
utils as insteon_utils,
)
from homeassistant.components.lock import ( # SERVICE_LOCK,; SERVICE_UNLOCK,
DOMAIN as LOCK_DOMAIN,
)
from homeassistant.const import ( # ATTR_ENTITY_ID,;
EVENT_HOMEASSISTANT_STOP,
STATE_LOCKED,
STATE_UNLOCKED,
Platform,
)
from homeassistant.helpers import entity_registry as er
from .const import MOCK_USER_INPUT_PLM
from .mock_devices import MockDevices
from tests.common import MockConfigEntry
devices = MockDevices()
@pytest.fixture(autouse=True)
def lock_platform_only():
"""Only setup the lock and required base platforms to speed up tests."""
with patch(
"homeassistant.components.insteon.INSTEON_PLATFORMS",
(Platform.LOCK,),
):
yield
@pytest.fixture(autouse=True)
def patch_setup_and_devices():
"""Patch the Insteon setup process and devices."""
with patch.object(insteon, "async_connect", new=mock_connection), patch.object(
insteon, "async_close"
), patch.object(insteon, "devices", devices), patch.object(
insteon_utils, "devices", devices
), patch.object(
insteon_entity, "devices", devices
):
yield
async def mock_connection(*args, **kwargs):
"""Return a successful connection."""
return True
async def test_lock_lock(hass):
"""Test locking an Insteon lock device."""
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM)
config_entry.add_to_hass(hass)
registry_entity = er.async_get(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
try:
lock = registry_entity.async_get("lock.device_55_55_55_55_55_55")
state = hass.states.get(lock.entity_id)
assert state.state is STATE_UNLOCKED
# lock via UI
await hass.services.async_call(
LOCK_DOMAIN, "lock", {"entity_id": lock.entity_id}, blocking=True
)
assert devices["55.55.55"].async_lock.call_count == 1
finally:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
async def test_lock_unlock(hass):
"""Test locking an Insteon lock device."""
config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_INPUT_PLM)
config_entry.add_to_hass(hass)
registry_entity = er.async_get(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
devices["55.55.55"].groups[1].set_value(255)
try:
lock = registry_entity.async_get("lock.device_55_55_55_55_55_55")
state = hass.states.get(lock.entity_id)
assert state.state is STATE_LOCKED
# lock via UI
await hass.services.async_call(
LOCK_DOMAIN, "unlock", {"entity_id": lock.entity_id}, blocking=True
)
assert devices["55.55.55"].async_unlock.call_count == 1
finally:
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()