From dbd821a564fb3adb0e9e9720ba7e8716a3fc597e Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 17 May 2020 09:27:38 -0400 Subject: [PATCH] Change Insteon backend module to pyinsteon from insteonplm (#35198) * Migrate to pyinsteon from insteonplm * Rename devices entities * Print ALDB even if not loaded * Add relay to name map * Change insteonplm to pyinsteon * Update requirements_all correctly * Code review updates * async_set_speed receive std speed value * default speed to std medium value * Call async methods for fan on/off * Comment await required in loop * Remove emtpy and add codeowner * Make services async and remove async_add_job call * Remove extra logging * New device as async task and aldb load in loop * Place lock in context bloxk * Limiting lock to min * Remove .env file --- CODEOWNERS | 1 + homeassistant/components/insteon/__init__.py | 144 +++++---- .../components/insteon/binary_sensor.py | 86 +++--- homeassistant/components/insteon/const.py | 117 +++++--- homeassistant/components/insteon/cover.py | 39 +-- homeassistant/components/insteon/fan.py | 71 ++--- .../components/insteon/insteon_entity.py | 68 ++--- homeassistant/components/insteon/ipdb.py | 177 ++++++----- homeassistant/components/insteon/light.py | 37 +-- .../components/insteon/manifest.json | 6 +- homeassistant/components/insteon/schemas.py | 12 +- homeassistant/components/insteon/sensor.py | 31 -- homeassistant/components/insteon/switch.py | 57 +--- homeassistant/components/insteon/utils.py | 279 ++++++++++++------ requirements_all.txt | 6 +- 15 files changed, 617 insertions(+), 514 deletions(-) delete mode 100644 homeassistant/components/insteon/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index ff3ae7b8bb4..20336249168 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -194,6 +194,7 @@ homeassistant/components/input_datetime/* @home-assistant/core homeassistant/components/input_number/* @home-assistant/core homeassistant/components/input_select/* @home-assistant/core homeassistant/components/input_text/* @home-assistant/core +homeassistant/components/insteon/* @teharris1 homeassistant/components/integration/* @dgomes homeassistant/components/intent/* @home-assistant/core homeassistant/components/intesishome/* @jnimmo diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index ce17cc6c77d..c28c04f589d 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,7 +1,8 @@ """Support for INSTEON Modems (PLM and Hub).""" +import asyncio import logging -import insteonplm +from pyinsteon import async_close, async_connect, devices from homeassistant.const import ( CONF_HOST, @@ -24,21 +25,75 @@ from .const import ( CONF_SUBCAT, CONF_UNITCODE, CONF_X10, - CONF_X10_ALL_LIGHTS_OFF, - CONF_X10_ALL_LIGHTS_ON, - CONF_X10_ALL_UNITS_OFF, DOMAIN, - INSTEON_ENTITIES, + INSTEON_COMPONENTS, + ON_OFF_EVENTS, ) from .schemas import CONFIG_SCHEMA # noqa F440 -from .utils import async_register_services, register_new_device_callback +from .utils import ( + add_on_off_event_device, + async_register_services, + get_device_platforms, + register_new_device_callback, +) _LOGGER = logging.getLogger(__name__) +async def async_id_unknown_devices(config_dir): + """Send device ID commands to all unidentified devices.""" + await devices.async_load(id_devices=1) + for addr in devices: + device = devices[addr] + flags = True + for name in device.operating_flags: + if not device.operating_flags[name].is_loaded: + flags = False + break + if flags: + for name in device.properties: + if not device.properties[name].is_loaded: + flags = False + break + + # Cannot be done concurrently due to issues with the underlying protocol. + if not device.aldb.is_loaded or not flags: + await device.async_read_config() + + await devices.async_save(workdir=config_dir) + + +async def async_setup_platforms(hass, config): + """Initiate the connection and services.""" + tasks = [ + hass.helpers.discovery.async_load_platform(component, DOMAIN, {}, config) + for component in INSTEON_COMPONENTS + ] + await asyncio.gather(*tasks) + + for address in devices: + device = devices[address] + platforms = get_device_platforms(device) + if ON_OFF_EVENTS in platforms: + add_on_off_event_device(hass, device) + + _LOGGER.debug("Insteon device count: %s", len(devices)) + register_new_device_callback(hass, config) + async_register_services(hass) + + # Cannot be done concurrently due to issues with the underlying protocol. + for address in devices: + await devices[address].async_status() + await async_id_unknown_devices(hass.config.config_dir) + + +async def close_insteon_connection(*args): + """Close the Insteon connection.""" + await async_close() + + async def async_setup(hass, config): """Set up the connection to the modem.""" - insteon_modem = None conf = config[DOMAIN] port = conf.get(CONF_PORT) @@ -47,68 +102,50 @@ async def async_setup(hass, config): username = conf.get(CONF_HUB_USERNAME) password = conf.get(CONF_HUB_PASSWORD) hub_version = conf.get(CONF_HUB_VERSION) - overrides = conf.get(CONF_OVERRIDE, []) - x10_devices = conf.get(CONF_X10, []) - x10_all_units_off_housecode = conf.get(CONF_X10_ALL_UNITS_OFF) - x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) - x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) if host: - _LOGGER.info("Connecting to Insteon Hub on %s", host) - conn = await insteonplm.Connection.create( + _LOGGER.info("Connecting to Insteon Hub on %s:%d", host, ip_port) + else: + _LOGGER.info("Connecting to Insteon PLM on %s", port) + + try: + await async_connect( + device=port, host=host, port=ip_port, username=username, password=password, hub_version=hub_version, - loop=hass.loop, - workdir=hass.config.config_dir, - ) - else: - _LOGGER.info("Looking for Insteon PLM on %s", port) - conn = await insteonplm.Connection.create( - device=port, loop=hass.loop, workdir=hass.config.config_dir ) + except ConnectionError: + _LOGGER.error("Could not connect to Insteon modem") + return False + _LOGGER.info("Connection to Insteon modem successful") - insteon_modem = conn.protocol + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_insteon_connection) + conf = config[DOMAIN] + overrides = conf.get(CONF_OVERRIDE, []) + x10_devices = conf.get(CONF_X10, []) - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["modem"] = insteon_modem - hass.data[DOMAIN][INSTEON_ENTITIES] = set() - - register_new_device_callback(hass, config, insteon_modem) - async_register_services(hass, config, insteon_modem) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) + await devices.async_load( + workdir=hass.config.config_dir, id_devices=0, load_modem_aldb=0 + ) for device_override in overrides: - # # Override the device default capabilities for a specific address - # address = device_override.get("address") - for prop in device_override: - if prop in [CONF_CAT, CONF_SUBCAT]: - insteon_modem.devices.add_override(address, prop, device_override[prop]) - elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]: - insteon_modem.devices.add_override( - address, CONF_PRODUCT_KEY, device_override[prop] - ) + if not devices.get(address): + cat = device_override[CONF_CAT] + subcat = device_override[CONF_SUBCAT] + firmware = device_override.get(CONF_FIRMWARE) + if firmware is None: + firmware = device_override.get(CONF_PRODUCT_KEY, 0) + devices.set_id(address, cat, subcat, firmware) - if x10_all_units_off_housecode: - device = insteon_modem.add_x10_device( - x10_all_units_off_housecode, 20, "allunitsoff" - ) - if x10_all_lights_on_housecode: - device = insteon_modem.add_x10_device( - x10_all_lights_on_housecode, 21, "alllightson" - ) - if x10_all_lights_off_housecode: - device = insteon_modem.add_x10_device( - x10_all_lights_off_housecode, 22, "alllightsoff" - ) for device in x10_devices: housecode = device.get(CONF_HOUSECODE) unitcode = device.get(CONF_UNITCODE) - x10_type = "onoff" + x10_type = "on_off" steps = device.get(CONF_DIM_STEPS, 22) if device.get(CONF_PLATFORM) == "light": x10_type = "dimmable" @@ -117,8 +154,7 @@ async def async_setup(hass, config): _LOGGER.debug( "Adding X10 device to Insteon: %s %d %s", housecode, unitcode, x10_type ) - device = insteon_modem.add_x10_device(housecode, unitcode, x10_type) - if device and hasattr(device.states[0x01], "steps"): - device.states[0x01].steps = steps + device = devices.add_x10_device(housecode, unitcode, x10_type, steps) + asyncio.create_task(async_setup_platforms(hass, config)) return True diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 81c3c58ef12..cd74f738187 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -1,50 +1,69 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity +from pyinsteon.groups import ( + CO_SENSOR, + DOOR_SENSOR, + HEARTBEAT, + LEAK_SENSOR_WET, + LIGHT_SENSOR, + LOW_BATTERY, + MOTION_SENSOR, + OPEN_CLOSE_SENSOR, + SENSOR_MALFUNCTION, + SMOKE_SENSOR, + TEST_SENSOR, +) + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_LIGHT, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_OPENING, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DOMAIN, + BinarySensorEntity, +) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) SENSOR_TYPES = { - "openClosedSensor": "opening", - "ioLincSensor": "opening", - "motionSensor": "motion", - "doorSensor": "door", - "wetLeakSensor": "moisture", - "lightSensor": "light", - "batterySensor": "battery", + OPEN_CLOSE_SENSOR: DEVICE_CLASS_OPENING, + MOTION_SENSOR: DEVICE_CLASS_MOTION, + DOOR_SENSOR: DEVICE_CLASS_DOOR, + LEAK_SENSOR_WET: DEVICE_CLASS_MOISTURE, + LIGHT_SENSOR: DEVICE_CLASS_LIGHT, + LOW_BATTERY: DEVICE_CLASS_BATTERY, + CO_SENSOR: DEVICE_CLASS_GAS, + SMOKE_SENSOR: DEVICE_CLASS_SMOKE, + TEST_SENSOR: DEVICE_CLASS_SAFETY, + SENSOR_MALFUNCTION: DEVICE_CLASS_PROBLEM, + HEARTBEAT: DEVICE_CLASS_PROBLEM, } async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - name = device.states[state_key].name - if name != "dryLeakSensor": - _LOGGER.debug( - "Adding device %s entity %s to Binary Sensor platform", - device.address.hex, - name, - ) - - new_entity = InsteonBinarySensor(device, state_key) - - async_add_entities([new_entity]) + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonBinarySensorEntity, async_add_entities, discovery_info + ) -class InsteonBinarySensor(InsteonEntity, BinarySensorEntity): - """A Class for an Insteon device entity.""" +class InsteonBinarySensorEntity(InsteonEntity, BinarySensorEntity): + """A Class for an Insteon binary sensor entity.""" - def __init__(self, device, state_key): + def __init__(self, device, group): """Initialize the INSTEON binary sensor.""" - super().__init__(device, state_key) - self._sensor_type = SENSOR_TYPES.get(self._insteon_device_state.name) + super().__init__(device, group) + self._sensor_type = SENSOR_TYPES.get(self._insteon_device_group.name) @property def device_class(self): @@ -54,9 +73,4 @@ class InsteonBinarySensor(InsteonEntity, BinarySensorEntity): @property def is_on(self): """Return the boolean response if the node is on.""" - on_val = bool(self._insteon_device_state.value) - - if self._insteon_device_state.name in ["lightSensor", "ioLincSensor"]: - return not on_val - - return on_val + return bool(self._insteon_device_group.value) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py index b01409f49ff..950efd8dc7f 100644 --- a/homeassistant/components/insteon/const.py +++ b/homeassistant/components/insteon/const.py @@ -1,7 +1,46 @@ """Constants used by insteon component.""" +from pyinsteon.groups import ( + CO_SENSOR, + COVER, + DIMMABLE_FAN, + DIMMABLE_LIGHT, + DIMMABLE_LIGHT_MAIN, + DIMMABLE_OUTLET, + DOOR_SENSOR, + HEARTBEAT, + LEAK_SENSOR_WET, + LIGHT_SENSOR, + LOW_BATTERY, + MOTION_SENSOR, + NEW_SENSOR, + ON_OFF_OUTLET_BOTTOM, + ON_OFF_OUTLET_TOP, + ON_OFF_SWITCH, + ON_OFF_SWITCH_A, + ON_OFF_SWITCH_B, + ON_OFF_SWITCH_C, + ON_OFF_SWITCH_D, + ON_OFF_SWITCH_E, + ON_OFF_SWITCH_F, + ON_OFF_SWITCH_G, + ON_OFF_SWITCH_H, + ON_OFF_SWITCH_MAIN, + OPEN_CLOSE_SENSOR, + RELAY, + SENSOR_MALFUNCTION, + SMOKE_SENSOR, + TEST_SENSOR, +) DOMAIN = "insteon" -INSTEON_ENTITIES = "entities" + +INSTEON_COMPONENTS = [ + "binary_sensor", + "cover", + "fan", + "light", + "switch", +] CONF_IP_PORT = "ip_port" CONF_HUB_USERNAME = "username" @@ -40,6 +79,7 @@ SRV_SCENE_OFF = "scene_off" SIGNAL_LOAD_ALDB = "load_aldb" SIGNAL_PRINT_ALDB = "print_aldb" +SIGNAL_SAVE_DEVICES = "save_devices" HOUSECODES = [ "a", @@ -60,47 +100,42 @@ HOUSECODES = [ "p", ] -BUTTON_PRESSED_STATE_NAME = "onLevelButton" -EVENT_BUTTON_ON = "insteon.button_on" -EVENT_BUTTON_OFF = "insteon.button_off" +EVENT_GROUP_ON = "insteon.button_on" +EVENT_GROUP_OFF = "insteon.button_off" +EVENT_GROUP_ON_FAST = "insteon.button_on_fast" +EVENT_GROUP_OFF_FAST = "insteon.button_off_fast" EVENT_CONF_BUTTON = "button" - +ON_OFF_EVENTS = "on_off_events" STATE_NAME_LABEL_MAP = { - "keypadButtonA": "Button A", - "keypadButtonB": "Button B", - "keypadButtonC": "Button C", - "keypadButtonD": "Button D", - "keypadButtonE": "Button E", - "keypadButtonF": "Button F", - "keypadButtonG": "Button G", - "keypadButtonH": "Button H", - "keypadButtonMain": "Main", - "onOffButtonA": "Button A", - "onOffButtonB": "Button B", - "onOffButtonC": "Button C", - "onOffButtonD": "Button D", - "onOffButtonE": "Button E", - "onOffButtonF": "Button F", - "onOffButtonG": "Button G", - "onOffButtonH": "Button H", - "onOffButtonMain": "Main", - "fanOnLevel": "Fan", - "lightOnLevel": "Light", - "coolSetPoint": "Cool Set", - "heatSetPoint": "HeatSet", - "statusReport": "Status", - "generalSensor": "Sensor", - "motionSensor": "Motion", - "lightSensor": "Light", - "batterySensor": "Battery", - "dryLeakSensor": "Dry", - "wetLeakSensor": "Wet", - "heartbeatLeakSensor": "Heartbeat", - "openClosedRelay": "Relay", - "openClosedSensor": "Sensor", - "lightOnOff": "Light", - "outletTopOnOff": "Top", - "outletBottomOnOff": "Bottom", - "coverOpenLevel": "Cover", + DIMMABLE_LIGHT_MAIN: "Main", + ON_OFF_SWITCH_A: "Button A", + ON_OFF_SWITCH_B: "Button B", + ON_OFF_SWITCH_C: "Button C", + ON_OFF_SWITCH_D: "Button D", + ON_OFF_SWITCH_E: "Button E", + ON_OFF_SWITCH_F: "Button F", + ON_OFF_SWITCH_G: "Button G", + ON_OFF_SWITCH_H: "Button H", + ON_OFF_SWITCH_MAIN: "Main", + DIMMABLE_FAN: "Fan", + DIMMABLE_LIGHT: "Light", + DIMMABLE_OUTLET: "Outlet", + MOTION_SENSOR: "Motion", + LIGHT_SENSOR: "Light", + LOW_BATTERY: "Battery", + LEAK_SENSOR_WET: "Wet", + DOOR_SENSOR: "Door", + SMOKE_SENSOR: "Smoke", + CO_SENSOR: "Carbon Monoxide", + TEST_SENSOR: "Test", + NEW_SENSOR: "New", + SENSOR_MALFUNCTION: "Malfunction", + HEARTBEAT: "Heartbeat", + OPEN_CLOSE_SENSOR: "Sensor", + ON_OFF_SWITCH: "Light", + ON_OFF_OUTLET_TOP: "Top", + ON_OFF_OUTLET_BOTTOM: "Bottom", + COVER: "Cover", + RELAY: "Relay", } diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index b325a6ebd84..75c336822d7 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -4,6 +4,7 @@ import math from homeassistant.components.cover import ( ATTR_POSITION, + DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_SET_POSITION, @@ -11,6 +12,7 @@ from homeassistant.components.cover import ( ) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) @@ -19,33 +21,18 @@ SUPPORTED_FEATURES = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Insteon platform.""" - if not discovery_info: - return - - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Cover platform", - device.address.hex, - device.states[state_key].name, + async_add_insteon_entities( + hass, DOMAIN, InsteonCoverEntity, async_add_entities, discovery_info ) - new_entity = InsteonCoverEntity(device, state_key) - - async_add_entities([new_entity]) - class InsteonCoverEntity(InsteonEntity, CoverEntity): - """A Class for an Insteon device.""" + """A Class for an Insteon cover entity.""" @property def current_cover_position(self): """Return the current cover position.""" - return int(math.ceil(self._insteon_device_state.value * 100 / 255)) + return int(math.ceil(self._insteon_device_group.value * 100 / 255)) @property def supported_features(self): @@ -58,17 +45,19 @@ class InsteonCoverEntity(InsteonEntity, CoverEntity): return bool(self.current_cover_position) async def async_open_cover(self, **kwargs): - """Open device.""" - self._insteon_device_state.open() + """Open cover.""" + await self._insteon_device.async_open() async def async_close_cover(self, **kwargs): - """Close device.""" - self._insteon_device_state.close() + """Close cover.""" + await self._insteon_device.async_close() async def async_set_cover_position(self, **kwargs): """Set the cover position.""" position = int(kwargs[ATTR_POSITION] * 255 / 100) if position == 0: - self._insteon_device_state.close() + await self._insteon_device.async_close() else: - self._insteon_device_state.set_position(position) + await self._insteon_device.async_open( + position=position, group=self._insteon_device_group.group + ) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index 6ad7436faf5..3b324b97782 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -1,7 +1,10 @@ """Support for INSTEON fans via PowerLinc Modem.""" import logging +from pyinsteon.constants import FanSpeed + from homeassistant.components.fan import ( + DOMAIN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -9,43 +12,40 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import STATE_OFF from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) - -SPEED_TO_HEX = {SPEED_OFF: 0x00, SPEED_LOW: 0x3F, SPEED_MEDIUM: 0xBE, SPEED_HIGH: 0xFF} - -FAN_SPEEDS = [STATE_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +FAN_SPEEDS = [SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] +SPEED_TO_VALUE = { + SPEED_OFF: FanSpeed.OFF, + SPEED_LOW: FanSpeed.LOW, + SPEED_MEDIUM: FanSpeed.MEDIUM, + SPEED_HIGH: FanSpeed.HIGH, +} async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Fan platform", - device.address.hex, - device.states[state_key].name, + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonFanEntity, async_add_entities, discovery_info ) - new_entity = InsteonFan(device, state_key) - async_add_entities([new_entity]) - - -class InsteonFan(InsteonEntity, FanEntity): - """An INSTEON fan component.""" +class InsteonFanEntity(InsteonEntity, FanEntity): + """An INSTEON fan entity.""" @property def speed(self) -> str: """Return the current speed.""" - return self._hex_to_speed(self._insteon_device_state.value) + if self._insteon_device_group.value == FanSpeed.HIGH: + return SPEED_HIGH + if self._insteon_device_group.value == FanSpeed.MEDIUM: + return SPEED_MEDIUM + if self._insteon_device_group.value == FanSpeed.LOW: + return SPEED_LOW + return SPEED_OFF @property def speed_list(self) -> list: @@ -58,30 +58,19 @@ class InsteonFan(InsteonEntity, FanEntity): return SUPPORT_SET_SPEED async def async_turn_on(self, speed: str = None, **kwargs) -> None: - """Turn on the entity.""" + """Turn on the fan.""" if speed is None: speed = SPEED_MEDIUM await self.async_set_speed(speed) async def async_turn_off(self, **kwargs) -> None: - """Turn off the entity.""" - await self.async_set_speed(SPEED_OFF) + """Turn off the fan.""" + await self._insteon_device.async_fan_off() async def async_set_speed(self, speed: str) -> None: """Set the speed of the fan.""" - fan_speed = SPEED_TO_HEX[speed] - if fan_speed == 0x00: - self._insteon_device_state.off() + fan_speed = SPEED_TO_VALUE[speed] + if fan_speed == FanSpeed.OFF: + await self._insteon_device.async_fan_off() else: - self._insteon_device_state.set_level(fan_speed) - - @staticmethod - def _hex_to_speed(speed: int): - hex_speed = SPEED_OFF - if speed > 0xFE: - hex_speed = SPEED_HIGH - elif speed > 0x7F: - hex_speed = SPEED_MEDIUM - elif speed > 0: - hex_speed = SPEED_LOW - return hex_speed + await self._insteon_device.async_fan_on(on_level=fan_speed) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py index b453cad2e07..80bb860477e 100644 --- a/homeassistant/components/insteon/insteon_entity.py +++ b/homeassistant/components/insteon/insteon_entity.py @@ -2,14 +2,16 @@ import logging from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from .const import ( - DOMAIN, - INSTEON_ENTITIES, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_SAVE_DEVICES, STATE_NAME_LABEL_MAP, ) from .utils import print_aldb_to_log @@ -20,11 +22,14 @@ _LOGGER = logging.getLogger(__name__) class InsteonEntity(Entity): """INSTEON abstract base entity.""" - def __init__(self, device, state_key): + def __init__(self, device, group): """Initialize the INSTEON binary sensor.""" - self._insteon_device_state = device.states[state_key] + self._insteon_device_group = device.groups[group] self._insteon_device = device - self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) + + def __hash__(self): + """Return the hash of the Insteon Entity.""" + return hash(self._insteon_device) @property def should_poll(self): @@ -34,20 +39,20 @@ class InsteonEntity(Entity): @property def address(self): """Return the address of the node.""" - return self._insteon_device.address.human + return str(self._insteon_device.address) @property def group(self): """Return the INSTEON group that the entity responds to.""" - return self._insteon_device_state.group + return self._insteon_device_group.group @property def unique_id(self) -> str: """Return a unique ID.""" - if self._insteon_device_state.group == 0x01: + if self._insteon_device_group.group == 0x01: uid = self._insteon_device.id else: - uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}" + uid = f"{self._insteon_device.id}_{self._insteon_device_group.group}" return uid @property @@ -61,7 +66,7 @@ class InsteonEntity(Entity): extension = self._get_label() if extension: extension = f" {extension}" - return f"{description} {self._insteon_device.address.human}{extension}" + return f"{description} {self._insteon_device.address}{extension}" @property def device_state_attributes(self): @@ -69,56 +74,45 @@ class InsteonEntity(Entity): return {"insteon_address": self.address, "insteon_group": self.group} @callback - def async_entity_update(self, deviceid, group, val): + def async_entity_update(self, name, address, value, group): """Receive notification from transport that new data exists.""" _LOGGER.debug( - "Received update for device %s group %d value %s", - deviceid.human, - group, - val, + "Received update for device %s group %d value %s", address, group, value, ) self.async_write_ha_state() async def async_added_to_hass(self): """Register INSTEON update events.""" _LOGGER.debug( - "Tracking updates for device %s group %d statename %s", + "Tracking updates for device %s group %d name %s", self.address, self.group, - self._insteon_device_state.name, + self._insteon_device_group.name, ) - self._insteon_device_state.register_updates(self.async_entity_update) - self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) + self._insteon_device_group.subscribe(self.async_entity_update) load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" self.async_on_remove( - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + async_dispatcher_connect(self.hass, load_signal, self._async_read_aldb) ) print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}" - self.async_on_remove( - async_dispatcher_connect(self.hass, print_signal, self._print_aldb) - ) + async_dispatcher_connect(self.hass, print_signal, self._print_aldb) - def _load_aldb(self, reload=False): - """Load the device All-Link Database.""" - if reload: - self._insteon_device.aldb.clear() - self._insteon_device.read_aldb() + async def _async_read_aldb(self, reload): + """Call device load process and print to log.""" + await self._insteon_device.aldb.async_load(refresh=reload) + self._print_aldb() + async_dispatcher_send(self.hass, SIGNAL_SAVE_DEVICES) def _print_aldb(self): """Print the device ALDB to the log file.""" print_aldb_to_log(self._insteon_device.aldb) - @callback - def _aldb_loaded(self): - """All-Link Database loaded for the device.""" - self._print_aldb() - def _get_label(self): """Get the device label for grouped devices.""" label = "" - if len(self._insteon_device.states) > 1: - if self._insteon_device_state.name in STATE_NAME_LABEL_MAP: - label = STATE_NAME_LABEL_MAP[self._insteon_device_state.name] + if len(self._insteon_device.groups) > 1: + if self._insteon_device_group.name in STATE_NAME_LABEL_MAP: + label = STATE_NAME_LABEL_MAP[self._insteon_device_group.name] else: label = f"Group {self.group:d}" return label diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py index 6aba40d6df9..5d0913185b1 100644 --- a/homeassistant/components/insteon/ipdb.py +++ b/homeassistant/components/insteon/ipdb.py @@ -1,81 +1,112 @@ -"""Insteon product database.""" -import collections +"""Utility methods for the Insteon platform.""" +import logging -from insteonplm.states.cover import Cover -from insteonplm.states.dimmable import ( - DimmableKeypadA, - DimmableRemote, - DimmableSwitch, - DimmableSwitch_Fan, -) -from insteonplm.states.onOff import ( - OnOffKeypad, - OnOffKeypadA, - OnOffSwitch, - OnOffSwitch_OutletBottom, - OnOffSwitch_OutletTop, - OpenClosedRelay, -) -from insteonplm.states.sensor import ( - IoLincSensor, - LeakSensorDryWet, - OnOffSensor, - SmokeCO2Sensor, - VariableSensor, -) -from insteonplm.states.x10 import ( - X10AllLightsOffSensor, - X10AllLightsOnSensor, - X10AllUnitsOffSensor, - X10DimmableSwitch, +from pyinsteon.device_types import ( + DimmableLightingControl, + DimmableLightingControl_DinRail, + DimmableLightingControl_FanLinc, + DimmableLightingControl_InLineLinc, + DimmableLightingControl_KeypadLinc_6, + DimmableLightingControl_KeypadLinc_8, + DimmableLightingControl_LampLinc, + DimmableLightingControl_OutletLinc, + DimmableLightingControl_SwitchLinc, + DimmableLightingControl_ToggleLinc, + GeneralController_ControlLinc, + GeneralController_MiniRemote_4, + GeneralController_MiniRemote_8, + GeneralController_MiniRemote_Switch, + GeneralController_RemoteLinc, + SecurityHealthSafety_DoorSensor, + SecurityHealthSafety_LeakSensor, + SecurityHealthSafety_MotionSensor, + SecurityHealthSafety_OpenCloseSensor, + SecurityHealthSafety_Smokebridge, + SensorsActuators_IOLink, + SwitchedLightingControl, + SwitchedLightingControl_ApplianceLinc, + SwitchedLightingControl_DinRail, + SwitchedLightingControl_InLineLinc, + SwitchedLightingControl_KeypadLinc_6, + SwitchedLightingControl_KeypadLinc_8, + SwitchedLightingControl_OnOffOutlet, + SwitchedLightingControl_OutletLinc, + SwitchedLightingControl_SwitchLinc, + SwitchedLightingControl_ToggleLinc, + WindowCovering, + X10Dimmable, + X10OnOff, X10OnOffSensor, - X10OnOffSwitch, ) -State = collections.namedtuple("Product", "stateType platform") +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +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.switch import DOMAIN as SWITCH + +from .const import ON_OFF_EVENTS + +_LOGGER = logging.getLogger(__name__) + +DEVICE_PLATFORM = { + 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]}, + DimmableLightingControl_InLineLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_KeypadLinc_6: { + LIGHT: [1], + SWITCH: [3, 4, 5, 6], + ON_OFF_EVENTS: [1, 3, 4, 5, 6], + }, + DimmableLightingControl_KeypadLinc_8: { + LIGHT: [1], + SWITCH: range(2, 9), + ON_OFF_EVENTS: range(1, 9), + }, + DimmableLightingControl_LampLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_OutletLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_SwitchLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + DimmableLightingControl_ToggleLinc: {LIGHT: [1], ON_OFF_EVENTS: [1]}, + GeneralController_ControlLinc: {ON_OFF_EVENTS: [1]}, + GeneralController_MiniRemote_4: {ON_OFF_EVENTS: range(1, 5)}, + GeneralController_MiniRemote_8: {ON_OFF_EVENTS: range(1, 9)}, + GeneralController_MiniRemote_Switch: {ON_OFF_EVENTS: [1, 2]}, + GeneralController_RemoteLinc: {ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_DoorSensor: {BINARY_SENSOR: [1, 3, 4], ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_LeakSensor: {BINARY_SENSOR: [2, 4]}, + SecurityHealthSafety_MotionSensor: {BINARY_SENSOR: [1, 2, 3], ON_OFF_EVENTS: [1]}, + SecurityHealthSafety_OpenCloseSensor: {BINARY_SENSOR: [1]}, + SecurityHealthSafety_Smokebridge: {BINARY_SENSOR: [1]}, + SensorsActuators_IOLink: {SWITCH: [1], BINARY_SENSOR: [2], ON_OFF_EVENTS: [1, 2]}, + SwitchedLightingControl: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_ApplianceLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_DinRail: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_InLineLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_KeypadLinc_6: { + SWITCH: [1, 3, 4, 5, 6], + ON_OFF_EVENTS: [1, 3, 4, 5, 6], + }, + SwitchedLightingControl_KeypadLinc_8: { + SWITCH: range(1, 9), + ON_OFF_EVENTS: range(1, 9), + }, + SwitchedLightingControl_OnOffOutlet: {SWITCH: [1, 2], ON_OFF_EVENTS: [1, 2]}, + SwitchedLightingControl_OutletLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_SwitchLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + SwitchedLightingControl_ToggleLinc: {SWITCH: [1], ON_OFF_EVENTS: [1]}, + WindowCovering: {COVER: [1]}, + X10Dimmable: {LIGHT: [1]}, + X10OnOff: {SWITCH: [1]}, + X10OnOffSensor: {BINARY_SENSOR: [1]}, +} -class IPDB: - """Embodies the INSTEON Product Database static data and access methods.""" +def get_device_platforms(device): + """Return the HA platforms for a device type.""" + return DEVICE_PLATFORM.get(type(device), {}).keys() - def __init__(self): - """Create the INSTEON Product Database (IPDB).""" - self.states = [ - State(Cover, "cover"), - State(OnOffSwitch_OutletTop, "switch"), - State(OnOffSwitch_OutletBottom, "switch"), - State(OpenClosedRelay, "switch"), - State(OnOffSwitch, "switch"), - State(OnOffKeypadA, "switch"), - State(OnOffKeypad, "switch"), - State(LeakSensorDryWet, "binary_sensor"), - State(IoLincSensor, "binary_sensor"), - State(SmokeCO2Sensor, "sensor"), - State(OnOffSensor, "binary_sensor"), - State(VariableSensor, "sensor"), - State(DimmableSwitch_Fan, "fan"), - State(DimmableSwitch, "light"), - State(DimmableRemote, "on_off_events"), - State(DimmableKeypadA, "light"), - State(X10DimmableSwitch, "light"), - State(X10OnOffSwitch, "switch"), - State(X10OnOffSensor, "binary_sensor"), - State(X10AllUnitsOffSensor, "binary_sensor"), - State(X10AllLightsOnSensor, "binary_sensor"), - State(X10AllLightsOffSensor, "binary_sensor"), - ] - def __len__(self): - """Return the number of INSTEON state types mapped to HA platforms.""" - return len(self.states) - - def __iter__(self): - """Itterate through the INSTEON state types to HA platforms.""" - yield from self.states - - def __getitem__(self, key): - """Return a Home Assistant platform from an INSTEON state type.""" - for state in self.states: - if isinstance(key, state.stateType): - return state - return None +def get_platform_groups(device, domain) -> dict: + """Return the platforms that a device belongs in.""" + return DEVICE_PLATFORM.get(type(device), {}).get(domain, {}) diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index afd575c363b..5ad02b6da5e 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, + DOMAIN, SUPPORT_BRIGHTNESS, LightEntity, ) from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) @@ -16,31 +18,18 @@ MAX_BRIGHTNESS = 255 async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Insteon component.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Light platform", - device.address.hex, - device.states[state_key].name, + async_add_insteon_entities( + hass, DOMAIN, InsteonDimmerEntity, async_add_entities, discovery_info ) - new_entity = InsteonDimmerDevice(device, state_key) - async_add_entities([new_entity]) - - -class InsteonDimmerDevice(InsteonEntity, LightEntity): - """A Class for an Insteon device.""" +class InsteonDimmerEntity(InsteonEntity, LightEntity): + """A Class for an Insteon light entity.""" @property def brightness(self): """Return the brightness of this light between 0..255.""" - onlevel = self._insteon_device_state.value - return int(onlevel) + return self._insteon_device_group.value @property def is_on(self): @@ -53,13 +42,15 @@ class InsteonDimmerDevice(InsteonEntity, LightEntity): return SUPPORT_BRIGHTNESS async def async_turn_on(self, **kwargs): - """Turn device on.""" + """Turn light on.""" if ATTR_BRIGHTNESS in kwargs: brightness = int(kwargs[ATTR_BRIGHTNESS]) - self._insteon_device_state.set_level(brightness) + await self._insteon_device.async_on( + on_level=brightness, group=self._insteon_device_group.group + ) else: - self._insteon_device_state.on() + await self._insteon_device.async_on(group=self._insteon_device_group.group) async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.off() + """Turn light off.""" + await self._insteon_device.async_off(self._insteon_device_group.group) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 8410c6b6ef4..87ad80047d8 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,6 +2,6 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.8"], - "codeowners": [] -} + "requirements": ["pyinsteon==1.0.0"], + "codeowners": ["@teharris1"] +} \ No newline at end of file diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py index e3f2644ac56..b3192fc8f66 100644 --- a/homeassistant/components/insteon/schemas.py +++ b/homeassistant/components/insteon/schemas.py @@ -11,7 +11,6 @@ from homeassistant.const import ( CONF_PLATFORM, CONF_PORT, ENTITY_MATCH_ALL, - ENTITY_MATCH_NONE, ) import homeassistant.helpers.config_validation as cv @@ -57,7 +56,6 @@ def set_default_port(schema: Dict) -> Dict: CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( - cv.deprecated(CONF_PLATFORM), vol.Schema( { vol.Required(CONF_ADDRESS): cv.string, @@ -86,6 +84,9 @@ CONF_X10_SCHEMA = vol.All( CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( + cv.deprecated(CONF_X10_ALL_UNITS_OFF), + cv.deprecated(CONF_X10_ALL_LIGHTS_ON), + cv.deprecated(CONF_X10_ALL_LIGHTS_OFF), vol.Schema( { vol.Exclusive( @@ -101,9 +102,6 @@ CONFIG_SCHEMA = vol.Schema( vol.Optional(CONF_OVERRIDE): vol.All( cv.ensure_list_csv, [CONF_DEVICE_OVERRIDE_SCHEMA] ), - vol.Optional(CONF_X10_ALL_UNITS_OFF): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_ON): vol.In(HOUSECODES), - vol.Optional(CONF_X10_ALL_LIGHTS_OFF): vol.In(HOUSECODES), vol.Optional(CONF_X10): vol.All( cv.ensure_list_csv, [CONF_X10_SCHEMA] ), @@ -134,9 +132,7 @@ DEL_ALL_LINK_SCHEMA = vol.Schema( LOAD_ALDB_SCHEMA = vol.Schema( { - vol.Required(CONF_ENTITY_ID): vol.Any( - cv.entity_id, ENTITY_MATCH_ALL, ENTITY_MATCH_NONE - ), + vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, } ) diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py deleted file mode 100644 index 475723b105d..00000000000 --- a/homeassistant/components/insteon/sensor.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Support for INSTEON dimmers via PowerLinc Modem.""" -import logging - -from homeassistant.helpers.entity import Entity - -from .insteon_entity import InsteonEntity - -_LOGGER = logging.getLogger(__name__) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - _LOGGER.debug( - "Adding device %s entity %s to Sensor platform", - device.address.hex, - device.states[state_key].name, - ) - - new_entity = InsteonSensorDevice(device, state_key) - - async_add_entities([new_entity]) - - -class InsteonSensorDevice(InsteonEntity, Entity): - """A Class for an Insteon device.""" diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index 3a0668459c9..9d4e12b0b46 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -1,66 +1,33 @@ """Support for INSTEON dimmers via PowerLinc Modem.""" import logging -from homeassistant.components.switch import SwitchEntity +from homeassistant.components.switch import DOMAIN, SwitchEntity from .insteon_entity import InsteonEntity +from .utils import async_add_insteon_entities _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the INSTEON device class for the hass platform.""" - insteon_modem = hass.data["insteon"].get("modem") - - address = discovery_info["address"] - device = insteon_modem.devices[address] - state_key = discovery_info["state_key"] - - state_name = device.states[state_key].name - - _LOGGER.debug( - "Adding device %s entity %s to Switch platform", device.address.hex, state_name, + """Set up the INSTEON entity class for the hass platform.""" + async_add_insteon_entities( + hass, DOMAIN, InsteonSwitchEntity, async_add_entities, discovery_info ) - new_entity = None - if state_name == "openClosedRelay": - new_entity = InsteonOpenClosedDevice(device, state_key) - else: - new_entity = InsteonSwitchDevice(device, state_key) - if new_entity is not None: - async_add_entities([new_entity]) - - -class InsteonSwitchDevice(InsteonEntity, SwitchEntity): - """A Class for an Insteon device.""" +class InsteonSwitchEntity(InsteonEntity, SwitchEntity): + """A Class for an Insteon switch entity.""" @property def is_on(self): """Return the boolean response if the node is on.""" - return bool(self._insteon_device_state.value) + return bool(self._insteon_device_group.value) async def async_turn_on(self, **kwargs): - """Turn device on.""" - self._insteon_device_state.on() + """Turn switch on.""" + await self._insteon_device.async_on(group=self._insteon_device_group.group) async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.off() - - -class InsteonOpenClosedDevice(InsteonEntity, SwitchEntity): - """A Class for an Insteon device.""" - - @property - def is_on(self): - """Return the boolean response if the node is on.""" - return bool(self._insteon_device_state.value) - - async def async_turn_on(self, **kwargs): - """Turn device on.""" - self._insteon_device_state.open() - - async def async_turn_off(self, **kwargs): - """Turn device off.""" - self._insteon_device_state.close() + """Turn switch off.""" + await self._insteon_device.async_off(group=self._insteon_device_group.group) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 26768936291..c0b93d93485 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -1,23 +1,44 @@ """Utilities used by insteon component.""" - +import asyncio import logging -from insteonplm.devices import ALDBStatus +from pyinsteon import devices +from pyinsteon.constants import ALDBStatus +from pyinsteon.events import OFF_EVENT, OFF_FAST_EVENT, ON_EVENT, ON_FAST_EVENT +from pyinsteon.managers.link_manager import ( + async_enter_linking_mode, + async_enter_unlinking_mode, +) +from pyinsteon.managers.scene_manager import ( + async_trigger_scene_off, + async_trigger_scene_on, +) +from pyinsteon.managers.x10_manager import ( + async_x10_all_lights_off, + async_x10_all_lights_on, + async_x10_all_units_off, +) from homeassistant.const import CONF_ADDRESS, CONF_ENTITY_ID, ENTITY_MATCH_ALL from homeassistant.core import callback from homeassistant.helpers import discovery -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, + dispatcher_send, +) from .const import ( - BUTTON_PRESSED_STATE_NAME, DOMAIN, - EVENT_BUTTON_OFF, - EVENT_BUTTON_ON, EVENT_CONF_BUTTON, - INSTEON_ENTITIES, + EVENT_GROUP_OFF, + EVENT_GROUP_OFF_FAST, + EVENT_GROUP_ON, + EVENT_GROUP_ON_FAST, + ON_OFF_EVENTS, SIGNAL_LOAD_ALDB, SIGNAL_PRINT_ALDB, + SIGNAL_SAVE_DEVICES, SRV_ADD_ALL_LINK, SRV_ALL_LINK_GROUP, SRV_ALL_LINK_MODE, @@ -34,7 +55,7 @@ from .const import ( SRV_X10_ALL_LIGHTS_ON, SRV_X10_ALL_UNITS_OFF, ) -from .ipdb import IPDB +from .ipdb import get_device_platforms, get_platform_groups from .schemas import ( ADD_ALL_LINK_SCHEMA, DEL_ALL_LINK_SCHEMA, @@ -47,91 +68,129 @@ from .schemas import ( _LOGGER = logging.getLogger(__name__) -def register_new_device_callback(hass, config, insteon_modem): - """Register callback for new Insteon device.""" - - def _fire_button_on_off_event(address, group, val): - # Firing an event when a button is pressed. - device = insteon_modem.devices[address.hex] - state_name = device.states[group].name - button = ( - "" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower() - ) - schema = {CONF_ADDRESS: address.hex} - if button: - schema[EVENT_CONF_BUTTON] = button - event = EVENT_BUTTON_ON if val else EVENT_BUTTON_OFF - _LOGGER.debug( - "Firing event %s with address %s and button %s", event, address.hex, button - ) - hass.bus.fire(event, schema) +def add_on_off_event_device(hass, device): + """Register an Insteon device as an on/off event device.""" @callback - def async_new_insteon_device(device): + def async_fire_group_on_off_event(name, address, group, button): + # Firing an event when a button is pressed. + if button and button[-2] == "_": + button_id = button[-1].lower() + else: + button_id = None + + schema = {CONF_ADDRESS: address} + if button_id: + schema[EVENT_CONF_BUTTON] = button_id + if name == ON_EVENT: + event = EVENT_GROUP_ON + if name == OFF_EVENT: + event = EVENT_GROUP_OFF + if name == ON_FAST_EVENT: + event = EVENT_GROUP_ON_FAST + if name == OFF_FAST_EVENT: + event = EVENT_GROUP_OFF_FAST + _LOGGER.debug("Firing event %s with %s", event, schema) + hass.bus.async_fire(event, schema) + + for group in device.events: + if isinstance(group, int): + for event in device.events[group]: + if event in [ + OFF_EVENT, + ON_EVENT, + OFF_FAST_EVENT, + ON_FAST_EVENT, + ]: + _LOGGER.debug( + "Registering on/off event for %s %d %s", + str(device.address), + group, + event, + ) + device.events[group][event].subscribe( + async_fire_group_on_off_event, force_strong_ref=True + ) + + +def register_new_device_callback(hass, config): + """Register callback for new Insteon device.""" + new_device_lock = asyncio.Lock() + + @callback + def async_new_insteon_device(address=None): """Detect device from transport to be delegated to platform.""" - ipdb = IPDB() - for state_key in device.states: - platform_info = ipdb[device.states[state_key]] - if platform_info and platform_info.platform: - platform = platform_info.platform + hass.async_create_task(async_create_new_entities(address)) - if platform == "on_off_events": - device.states[state_key].register_updates(_fire_button_on_off_event) + async def async_create_new_entities(address): + _LOGGER.debug( + "Adding new INSTEON device to Home Assistant with address %s", address + ) + async with new_device_lock: + await devices.async_save(workdir=hass.config.config_dir) + device = devices[address] + await device.async_status() + platforms = get_device_platforms(device) + tasks = [] + for platform in platforms: + if platform == ON_OFF_EVENTS: + add_on_off_event_device(hass, device) - else: - _LOGGER.info( - "New INSTEON device: %s (%s) %s", - device.address, - device.states[state_key].name, + else: + tasks.append( + discovery.async_load_platform( + hass, platform, + DOMAIN, + discovered={"address": device.address.id}, + hass_config=config, ) + ) + await asyncio.gather(*tasks) - hass.async_create_task( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - discovered={ - "address": device.address.id, - "state_key": state_key, - }, - hass_config=config, - ) - ) - - insteon_modem.devices.add_device_callback(async_new_insteon_device) + devices.subscribe(async_new_insteon_device, force_strong_ref=True) @callback -def async_register_services(hass, config, insteon_modem): +def async_register_services(hass): """Register services used by insteon component.""" - def add_all_link(service): + async def async_srv_add_all_link(service): """Add an INSTEON All-Link between two devices.""" group = service.data.get(SRV_ALL_LINK_GROUP) mode = service.data.get(SRV_ALL_LINK_MODE) - link_mode = 1 if mode.lower() == SRV_CONTROLLER else 0 - insteon_modem.start_all_linking(link_mode, group) + link_mode = mode.lower() == SRV_CONTROLLER + await async_enter_linking_mode(link_mode, group) - def del_all_link(service): + async def async_srv_del_all_link(service): """Delete an INSTEON All-Link between two devices.""" group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.start_all_linking(255, group) + await async_enter_unlinking_mode(group) - def load_aldb(service): + async def async_srv_load_aldb(service): """Load the device All-Link database.""" entity_id = service.data[CONF_ENTITY_ID] reload = service.data[SRV_LOAD_DB_RELOAD] if entity_id.lower() == ENTITY_MATCH_ALL: - for entity_id in hass.data[DOMAIN][INSTEON_ENTITIES]: - _send_load_aldb_signal(entity_id, reload) + await async_srv_load_aldb_all(reload) else: - _send_load_aldb_signal(entity_id, reload) + signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" + async_dispatcher_send(hass, signal, reload) - def _send_load_aldb_signal(entity_id, reload): - """Send the load All-Link database signal to INSTEON entity.""" - signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}" - dispatcher_send(hass, signal, reload) + async def async_srv_load_aldb_all(reload): + """Load the All-Link database for all devices.""" + # Cannot be done concurrently due to issues with the underlying protocol. + for address in devices: + device = devices[address] + if device != devices.modem and device.cat != 0x03: + await device.aldb.async_load( + refresh=reload, callback=async_srv_save_devices + ) + + async def async_srv_save_devices(): + """Write the Insteon device configuration to file.""" + _LOGGER.debug("Saving Insteon devices") + await devices.async_save(hass.config.config_dir) def print_aldb(service): """Print the All-Link Database for a device.""" @@ -145,71 +204,85 @@ def async_register_services(hass, config, insteon_modem): """Print the All-Link Database for a device.""" # For now this sends logs to the log file. # Future direction is to create an INSTEON control panel. - print_aldb_to_log(insteon_modem.aldb) + print_aldb_to_log(devices.modem.aldb) - def x10_all_units_off(service): + async def async_srv_x10_all_units_off(service): """Send the X10 All Units Off command.""" housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_units_off(housecode) + await async_x10_all_units_off(housecode) - def x10_all_lights_off(service): + async def async_srv_x10_all_lights_off(service): """Send the X10 All Lights Off command.""" housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_off(housecode) + await async_x10_all_lights_off(housecode) - def x10_all_lights_on(service): + async def async_srv_x10_all_lights_on(service): """Send the X10 All Lights On command.""" housecode = service.data.get(SRV_HOUSECODE) - insteon_modem.x10_all_lights_on(housecode) + await async_x10_all_lights_on(housecode) - def scene_on(service): + async def async_srv_scene_on(service): """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_on(group) + await async_trigger_scene_on(group) - def scene_off(service): + async def async_srv_scene_off(service): """Trigger an INSTEON scene ON.""" group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_off(group) + await async_trigger_scene_off(group) hass.services.async_register( - DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA + DOMAIN, SRV_ADD_ALL_LINK, async_srv_add_all_link, schema=ADD_ALL_LINK_SCHEMA ) hass.services.async_register( - DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA + DOMAIN, SRV_DEL_ALL_LINK, async_srv_del_all_link, schema=DEL_ALL_LINK_SCHEMA ) hass.services.async_register( - DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA + DOMAIN, SRV_LOAD_ALDB, async_srv_load_aldb, schema=LOAD_ALDB_SCHEMA ) hass.services.async_register( DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA ) hass.services.async_register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) hass.services.async_register( - DOMAIN, SRV_X10_ALL_UNITS_OFF, x10_all_units_off, schema=X10_HOUSECODE_SCHEMA + DOMAIN, + SRV_X10_ALL_UNITS_OFF, + async_srv_x10_all_units_off, + schema=X10_HOUSECODE_SCHEMA, ) hass.services.async_register( - DOMAIN, SRV_X10_ALL_LIGHTS_OFF, x10_all_lights_off, schema=X10_HOUSECODE_SCHEMA + DOMAIN, + SRV_X10_ALL_LIGHTS_OFF, + async_srv_x10_all_lights_off, + schema=X10_HOUSECODE_SCHEMA, ) hass.services.async_register( - DOMAIN, SRV_X10_ALL_LIGHTS_ON, x10_all_lights_on, schema=X10_HOUSECODE_SCHEMA + DOMAIN, + SRV_X10_ALL_LIGHTS_ON, + async_srv_x10_all_lights_on, + schema=X10_HOUSECODE_SCHEMA, ) hass.services.async_register( - DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA + DOMAIN, SRV_SCENE_ON, async_srv_scene_on, schema=TRIGGER_SCENE_SCHEMA ) hass.services.async_register( - DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA + DOMAIN, SRV_SCENE_OFF, async_srv_scene_off, schema=TRIGGER_SCENE_SCHEMA ) + async_dispatcher_connect(hass, SIGNAL_SAVE_DEVICES, async_srv_save_devices) _LOGGER.debug("Insteon Services registered") def print_aldb_to_log(aldb): """Print the All-Link Database to the log file.""" - _LOGGER.info("ALDB load status is %s", aldb.status.name) + # This service is useless if the log level is not INFO for the + # insteon component. Setting the log level to INFO and resetting it + # back when we are done + orig_log_level = _LOGGER.level + if orig_log_level > logging.INFO: + _LOGGER.setLevel(logging.INFO) + _LOGGER.info("%s ALDB load status is %s", aldb.address, aldb.status.name) if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]: - _LOGGER.warning("Device All-Link database not loaded") - _LOGGER.warning("Use service insteon.load_aldb first") - return + _LOGGER.warning("All-Link database not loaded") _LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") @@ -217,12 +290,30 @@ def print_aldb_to_log(aldb): rec = aldb[mem_addr] # For now we write this to the log # Roadmap is to create a configuration panel - in_use = "Y" if rec.control_flags.is_in_use else "N" - mode = "C" if rec.control_flags.is_controller else "R" - hwm = "Y" if rec.control_flags.is_high_water_mark else "N" + in_use = "Y" if rec.is_in_use else "N" + mode = "C" if rec.is_controller else "R" + hwm = "Y" if rec.is_high_water_mark else "N" log_msg = ( f" {rec.mem_addr:04x} {in_use:s} {mode:s} {hwm:s} " - f"{rec.group:3d} {rec.address.human:s} {rec.data1:3d} " + f"{rec.group:3d} {str(rec.target):s} {rec.data1:3d} " f"{rec.data2:3d} {rec.data3:3d}" ) _LOGGER.info(log_msg) + _LOGGER.setLevel(orig_log_level) + + +@callback +def async_add_insteon_entities( + hass, platform, entity_type, async_add_entities, discovery_info +): + """Add Insteon devices to a platform.""" + new_entities = [] + device_list = [discovery_info.get("address")] if discovery_info else devices + + for address in device_list: + device = devices[address] + groups = get_platform_groups(device, platform) + for group in groups: + new_entities.append(entity_type(device, group)) + if new_entities: + async_add_entities(new_entities) diff --git a/requirements_all.txt b/requirements_all.txt index 264750a0989..ce77e2c1148 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -787,9 +787,6 @@ incomfort-client==0.4.0 # homeassistant.components.influxdb influxdb==5.2.3 -# homeassistant.components.insteon -insteonplm==0.16.8 - # homeassistant.components.iperf3 iperf3==0.1.11 @@ -1377,6 +1374,9 @@ pyialarm==0.3 # homeassistant.components.icloud pyicloud==0.9.7 +# homeassistant.components.insteon +pyinsteon==1.0.0 + # homeassistant.components.intesishome pyintesishome==1.7.4