From 7ff30fe29d2cc4e3af74cc7ffc01677ddc6f0ea6 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Thu, 30 Jan 2020 04:47:44 -0500 Subject: [PATCH] Reorganize insteon code (#31183) * Reorganize code * Code review --- homeassistant/components/insteon/__init__.py | 649 +----------------- .../components/insteon/binary_sensor.py | 2 +- homeassistant/components/insteon/const.py | 106 +++ homeassistant/components/insteon/cover.py | 2 +- homeassistant/components/insteon/fan.py | 2 +- .../components/insteon/insteon_entity.py | 123 ++++ homeassistant/components/insteon/ipdb.py | 82 +++ homeassistant/components/insteon/light.py | 2 +- homeassistant/components/insteon/schemas.py | 153 +++++ homeassistant/components/insteon/sensor.py | 2 +- homeassistant/components/insteon/switch.py | 2 +- homeassistant/components/insteon/utils.py | 239 +++++++ 12 files changed, 740 insertions(+), 624 deletions(-) create mode 100644 homeassistant/components/insteon/const.py create mode 100644 homeassistant/components/insteon/insteon_entity.py create mode 100644 homeassistant/components/insteon/ipdb.py create mode 100644 homeassistant/components/insteon/schemas.py create mode 100644 homeassistant/components/insteon/utils.py diff --git a/homeassistant/components/insteon/__init__.py b/homeassistant/components/insteon/__init__.py index df6fa626a4f..ce17cc6c77d 100644 --- a/homeassistant/components/insteon/__init__.py +++ b/homeassistant/components/insteon/__init__.py @@ -1,278 +1,43 @@ """Support for INSTEON Modems (PLM and Hub).""" -import collections import logging -from typing import Dict import insteonplm -from insteonplm.devices import ALDBStatus -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, - X10OnOffSensor, - X10OnOffSwitch, -) -import voluptuous as vol from homeassistant.const import ( - CONF_ADDRESS, - CONF_ENTITY_ID, CONF_HOST, CONF_PLATFORM, CONF_PORT, - ENTITY_MATCH_ALL, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.core import callback -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + INSTEON_ENTITIES, +) +from .schemas import CONFIG_SCHEMA # noqa F440 +from .utils import async_register_services, register_new_device_callback _LOGGER = logging.getLogger(__name__) -DOMAIN = "insteon" -INSTEON_ENTITIES = "entities" - -CONF_IP_PORT = "ip_port" -CONF_HUB_USERNAME = "username" -CONF_HUB_PASSWORD = "password" -CONF_HUB_VERSION = "hub_version" -CONF_OVERRIDE = "device_override" -CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" -CONF_CAT = "cat" -CONF_SUBCAT = "subcat" -CONF_FIRMWARE = "firmware" -CONF_PRODUCT_KEY = "product_key" -CONF_X10 = "x10_devices" -CONF_HOUSECODE = "housecode" -CONF_UNITCODE = "unitcode" -CONF_DIM_STEPS = "dim_steps" -CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" -CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" - -SRV_ADD_ALL_LINK = "add_all_link" -SRV_DEL_ALL_LINK = "delete_all_link" -SRV_LOAD_ALDB = "load_all_link_database" -SRV_PRINT_ALDB = "print_all_link_database" -SRV_PRINT_IM_ALDB = "print_im_all_link_database" -SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" -SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" -SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" -SRV_ALL_LINK_GROUP = "group" -SRV_ALL_LINK_MODE = "mode" -SRV_LOAD_DB_RELOAD = "reload" -SRV_CONTROLLER = "controller" -SRV_RESPONDER = "responder" -SRV_HOUSECODE = "housecode" -SRV_SCENE_ON = "scene_on" -SRV_SCENE_OFF = "scene_off" - -SIGNAL_LOAD_ALDB = "load_aldb" -SIGNAL_PRINT_ALDB = "print_aldb" - -HOUSECODES = [ - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", -] - -BUTTON_PRESSED_STATE_NAME = "onLevelButton" -EVENT_BUTTON_ON = "insteon.button_on" -EVENT_BUTTON_OFF = "insteon.button_off" -EVENT_CONF_BUTTON = "button" - - -def set_default_port(schema: Dict) -> Dict: - """Set the default port based on the Hub version.""" - # If the ip_port is found do nothing - # If it is not found the set the default - ip_port = schema.get(CONF_IP_PORT) - if not ip_port: - hub_version = schema.get(CONF_HUB_VERSION) - # Found hub_version but not ip_port - if hub_version == 1: - schema[CONF_IP_PORT] = 9761 - else: - schema[CONF_IP_PORT] = 25105 - return schema - - -CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( - cv.deprecated(CONF_PLATFORM), - vol.Schema( - { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_CAT): cv.byte, - vol.Optional(CONF_SUBCAT): cv.byte, - vol.Optional(CONF_FIRMWARE): cv.byte, - vol.Optional(CONF_PRODUCT_KEY): cv.byte, - vol.Optional(CONF_PLATFORM): cv.string, - } - ), -) - - -CONF_X10_SCHEMA = vol.All( - vol.Schema( - { - vol.Required(CONF_HOUSECODE): cv.string, - vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), - vol.Required(CONF_PLATFORM): cv.string, - vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), - } - ) -) - - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - vol.Schema( - { - vol.Exclusive( - CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Exclusive( - CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG - ): cv.string, - vol.Optional(CONF_IP_PORT): cv.port, - vol.Optional(CONF_HUB_USERNAME): cv.string, - vol.Optional(CONF_HUB_PASSWORD): cv.string, - vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), - 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] - ), - }, - extra=vol.ALLOW_EXTRA, - required=True, - ), - cv.has_at_least_one_key(CONF_PORT, CONF_HOST), - set_default_port, - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -ADD_ALL_LINK_SCHEMA = vol.Schema( - { - vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), - vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), - } -) - - -DEL_ALL_LINK_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -LOAD_ALDB_SCHEMA = vol.Schema( - { - vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), - vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, - } -) - - -PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) - - -X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) - - -TRIGGER_SCENE_SCHEMA = vol.Schema( - {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} -) - - -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", -} - async def async_setup(hass, config): """Set up the connection to the modem.""" - ipdb = IPDB() insteon_modem = None conf = config[DOMAIN] @@ -288,163 +53,6 @@ async def async_setup(hass, config): x10_all_lights_on_housecode = conf.get(CONF_X10_ALL_LIGHTS_ON) x10_all_lights_off_housecode = conf.get(CONF_X10_ALL_LIGHTS_OFF) - @callback - def async_new_insteon_device(device): - """Detect device from transport to be delegated to platform.""" - for state_key in device.states: - platform_info = ipdb[device.states[state_key]] - if platform_info and platform_info.platform: - platform = platform_info.platform - - if platform == "on_off_events": - device.states[state_key].register_updates(_fire_button_on_off_event) - - else: - _LOGGER.info( - "New INSTEON device: %s (%s) %s", - device.address, - device.states[state_key].name, - platform, - ) - - hass.async_create_task( - discovery.async_load_platform( - hass, - platform, - DOMAIN, - discovered={ - "address": device.address.id, - "state_key": state_key, - }, - hass_config=config, - ) - ) - - def 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) - - def 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) - - def 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].get(INSTEON_ENTITIES): - _send_load_aldb_signal(entity_id, reload) - else: - _send_load_aldb_signal(entity_id, 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) - - def print_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - entity_id = service.data[CONF_ENTITY_ID] - signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" - dispatcher_send(hass, signal) - - def print_im_aldb(service): - """Print the All-Link Database for a device.""" - # For now this sends logs to the log file. - # Furture direction is to create an INSTEON control panel. - print_aldb_to_log(insteon_modem.aldb) - - def 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) - - def 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) - - def 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) - - def scene_on(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_on(group) - - def scene_off(service): - """Trigger an INSTEON scene ON.""" - group = service.data.get(SRV_ALL_LINK_GROUP) - insteon_modem.trigger_group_off(group) - - def _register_services(): - hass.services.register( - DOMAIN, SRV_ADD_ALL_LINK, add_all_link, schema=ADD_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_DEL_ALL_LINK, del_all_link, schema=DEL_ALL_LINK_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_LOAD_ALDB, load_aldb, schema=LOAD_ALDB_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_PRINT_ALDB, print_aldb, schema=PRINT_ALDB_SCHEMA - ) - hass.services.register(DOMAIN, SRV_PRINT_IM_ALDB, print_im_aldb, schema=None) - hass.services.register( - DOMAIN, - SRV_X10_ALL_UNITS_OFF, - x10_all_units_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_OFF, - x10_all_lights_off, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, - SRV_X10_ALL_LIGHTS_ON, - x10_all_lights_on, - schema=X10_HOUSECODE_SCHEMA, - ) - hass.services.register( - DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA - ) - hass.services.register( - DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA - ) - _LOGGER.debug("Insteon Services registered") - - 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 - if val: - event = EVENT_BUTTON_ON - else: - event = EVENT_BUTTON_OFF - _LOGGER.debug( - "Firing event %s with address %s and button %s", event, address.hex, button - ) - hass.bus.fire(event, schema) - if host: _LOGGER.info("Connecting to Insteon Hub on %s", host) conn = await insteonplm.Connection.create( @@ -464,6 +72,14 @@ async def async_setup(hass, config): insteon_modem = conn.protocol + 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) + for device_override in overrides: # # Override the device default capabilities for a specific address @@ -477,14 +93,6 @@ async def async_setup(hass, config): address, CONF_PRODUCT_KEY, device_override[prop] ) - hass.data[DOMAIN] = {} - hass.data[DOMAIN]["modem"] = insteon_modem - hass.data[DOMAIN][INSTEON_ENTITIES] = {} - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close) - - insteon_modem.devices.add_device_callback(async_new_insteon_device) - if x10_all_units_off_housecode: device = insteon_modem.add_x10_device( x10_all_units_off_housecode, 20, "allunitsoff" @@ -513,199 +121,4 @@ async def async_setup(hass, config): if device and hasattr(device.states[0x01], "steps"): device.states[0x01].steps = steps - hass.async_add_job(_register_services) - return True - - -State = collections.namedtuple("Product", "stateType platform") - - -class IPDB: - """Embodies the INSTEON Product Database static data and access methods.""" - - 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.""" - for product in self.states: - yield product - - 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 - - -class InsteonEntity(Entity): - """INSTEON abstract base entity.""" - - def __init__(self, device, state_key): - """Initialize the INSTEON binary sensor.""" - self._insteon_device_state = device.states[state_key] - self._insteon_device = device - self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def address(self): - """Return the address of the node.""" - return self._insteon_device.address.human - - @property - def group(self): - """Return the INSTEON group that the entity responds to.""" - return self._insteon_device_state.group - - @property - def unique_id(self) -> str: - """Return a unique ID.""" - if self._insteon_device_state.group == 0x01: - uid = self._insteon_device.id - else: - uid = "{:s}_{:d}".format( - self._insteon_device.id, self._insteon_device_state.group - ) - return uid - - @property - def name(self): - """Return the name of the node (used for Entity_ID).""" - # Set a base description - description = self._insteon_device.description - if self._insteon_device.description is None: - description = "Unknown Device" - - # Get an extension label if there is one - extension = self._get_label() - if extension: - extension = f" {extension}" - name = "{:s} {:s}{:s}".format( - description, self._insteon_device.address.human, extension - ) - return name - - @property - def device_state_attributes(self): - """Provide attributes for display on device card.""" - attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group} - return attributes - - @callback - def async_entity_update(self, deviceid, group, val): - """Receive notification from transport that new data exists.""" - _LOGGER.debug( - "Received update for device %s group %d value %s", - deviceid.human, - group, - val, - ) - self.async_schedule_update_ha_state() - - async def async_added_to_hass(self): - """Register INSTEON update events.""" - _LOGGER.debug( - "Tracking updates for device %s group %d statename %s", - self.address, - self.group, - self._insteon_device_state.name, - ) - self._insteon_device_state.register_updates(self.async_entity_update) - self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self - load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" - async_dispatcher_connect(self.hass, load_signal, self._load_aldb) - print_signal = f"{self.entity_id}_{SIGNAL_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() - - 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] - else: - label = f"Group {self.group:d}" - return label - - -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) - 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.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") - _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") - for mem_addr in 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" - _LOGGER.info( - " {:04x} {:s} {:s} {:s} {:3d} {:s}" - " {:3d} {:3d} {:3d}".format( - rec.mem_addr, - in_use, - mode, - hwm, - rec.group, - rec.address.human, - rec.data1, - rec.data2, - rec.data3, - ) - ) diff --git a/homeassistant/components/insteon/binary_sensor.py b/homeassistant/components/insteon/binary_sensor.py index 68ea07cdb49..395c0a3ac20 100644 --- a/homeassistant/components/insteon/binary_sensor.py +++ b/homeassistant/components/insteon/binary_sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/const.py b/homeassistant/components/insteon/const.py new file mode 100644 index 00000000000..b01409f49ff --- /dev/null +++ b/homeassistant/components/insteon/const.py @@ -0,0 +1,106 @@ +"""Constants used by insteon component.""" + +DOMAIN = "insteon" +INSTEON_ENTITIES = "entities" + +CONF_IP_PORT = "ip_port" +CONF_HUB_USERNAME = "username" +CONF_HUB_PASSWORD = "password" +CONF_HUB_VERSION = "hub_version" +CONF_OVERRIDE = "device_override" +CONF_PLM_HUB_MSG = "Must configure either a PLM port or a Hub host" +CONF_CAT = "cat" +CONF_SUBCAT = "subcat" +CONF_FIRMWARE = "firmware" +CONF_PRODUCT_KEY = "product_key" +CONF_X10 = "x10_devices" +CONF_HOUSECODE = "housecode" +CONF_UNITCODE = "unitcode" +CONF_DIM_STEPS = "dim_steps" +CONF_X10_ALL_UNITS_OFF = "x10_all_units_off" +CONF_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +CONF_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" + +SRV_ADD_ALL_LINK = "add_all_link" +SRV_DEL_ALL_LINK = "delete_all_link" +SRV_LOAD_ALDB = "load_all_link_database" +SRV_PRINT_ALDB = "print_all_link_database" +SRV_PRINT_IM_ALDB = "print_im_all_link_database" +SRV_X10_ALL_UNITS_OFF = "x10_all_units_off" +SRV_X10_ALL_LIGHTS_OFF = "x10_all_lights_off" +SRV_X10_ALL_LIGHTS_ON = "x10_all_lights_on" +SRV_ALL_LINK_GROUP = "group" +SRV_ALL_LINK_MODE = "mode" +SRV_LOAD_DB_RELOAD = "reload" +SRV_CONTROLLER = "controller" +SRV_RESPONDER = "responder" +SRV_HOUSECODE = "housecode" +SRV_SCENE_ON = "scene_on" +SRV_SCENE_OFF = "scene_off" + +SIGNAL_LOAD_ALDB = "load_aldb" +SIGNAL_PRINT_ALDB = "print_aldb" + +HOUSECODES = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", +] + +BUTTON_PRESSED_STATE_NAME = "onLevelButton" +EVENT_BUTTON_ON = "insteon.button_on" +EVENT_BUTTON_OFF = "insteon.button_off" +EVENT_CONF_BUTTON = "button" + + +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", +} diff --git a/homeassistant/components/insteon/cover.py b/homeassistant/components/insteon/cover.py index f9399d7b13f..575799cbf67 100644 --- a/homeassistant/components/insteon/cover.py +++ b/homeassistant/components/insteon/cover.py @@ -10,7 +10,7 @@ from homeassistant.components.cover import ( CoverDevice, ) -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/fan.py b/homeassistant/components/insteon/fan.py index d88348b1a5d..6ad7436faf5 100644 --- a/homeassistant/components/insteon/fan.py +++ b/homeassistant/components/insteon/fan.py @@ -11,7 +11,7 @@ from homeassistant.components.fan import ( ) from homeassistant.const import STATE_OFF -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/insteon_entity.py b/homeassistant/components/insteon/insteon_entity.py new file mode 100644 index 00000000000..c489dd8e382 --- /dev/null +++ b/homeassistant/components/insteon/insteon_entity.py @@ -0,0 +1,123 @@ +"""Insteon base entity.""" +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .const import ( + DOMAIN, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + STATE_NAME_LABEL_MAP, +) +from .utils import print_aldb_to_log + +_LOGGER = logging.getLogger(__name__) + + +class InsteonEntity(Entity): + """INSTEON abstract base entity.""" + + def __init__(self, device, state_key): + """Initialize the INSTEON binary sensor.""" + self._insteon_device_state = device.states[state_key] + self._insteon_device = device + self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded) + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def address(self): + """Return the address of the node.""" + return self._insteon_device.address.human + + @property + def group(self): + """Return the INSTEON group that the entity responds to.""" + return self._insteon_device_state.group + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + if self._insteon_device_state.group == 0x01: + uid = self._insteon_device.id + else: + uid = f"{self._insteon_device.id}_{self._insteon_device_state.group}" + return uid + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + # Set a base description + description = self._insteon_device.description + if self._insteon_device.description is None: + description = "Unknown Device" + + # Get an extension label if there is one + extension = self._get_label() + if extension: + extension = f" {extension}" + name = f"{description} {self._insteon_device.address.human}{extension}" + return name + + @property + def device_state_attributes(self): + """Provide attributes for display on device card.""" + attributes = {"insteon_address": self.address, "insteon_group": self.group} + return attributes + + @callback + def async_entity_update(self, deviceid, group, val): + """Receive notification from transport that new data exists.""" + _LOGGER.debug( + "Received update for device %s group %d value %s", + deviceid.human, + group, + val, + ) + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Register INSTEON update events.""" + _LOGGER.debug( + "Tracking updates for device %s group %d statename %s", + self.address, + self.group, + self._insteon_device_state.name, + ) + self._insteon_device_state.register_updates(self.async_entity_update) + self.hass.data[DOMAIN][INSTEON_ENTITIES].add(self.entity_id) + load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}" + async_dispatcher_connect(self.hass, load_signal, self._load_aldb) + print_signal = f"{self.entity_id}_{SIGNAL_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() + + 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] + else: + label = f"Group {self.group:d}" + return label diff --git a/homeassistant/components/insteon/ipdb.py b/homeassistant/components/insteon/ipdb.py new file mode 100644 index 00000000000..1618518a0eb --- /dev/null +++ b/homeassistant/components/insteon/ipdb.py @@ -0,0 +1,82 @@ +"""Insteon product database.""" +import collections + +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, + X10OnOffSensor, + X10OnOffSwitch, +) + +State = collections.namedtuple("Product", "stateType platform") + + +class IPDB: + """Embodies the INSTEON Product Database static data and access methods.""" + + 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.""" + for product in self.states: + yield product + + 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 diff --git a/homeassistant/components/insteon/light.py b/homeassistant/components/insteon/light.py index 3a44d89add0..60a27b3acb8 100644 --- a/homeassistant/components/insteon/light.py +++ b/homeassistant/components/insteon/light.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/schemas.py b/homeassistant/components/insteon/schemas.py new file mode 100644 index 00000000000..1ae4ebed99e --- /dev/null +++ b/homeassistant/components/insteon/schemas.py @@ -0,0 +1,153 @@ +"""Schemas used by insteon component.""" + +from typing import Dict + +import voluptuous as vol + +from homeassistant.const import ( + CONF_ADDRESS, + CONF_ENTITY_ID, + CONF_HOST, + CONF_PLATFORM, + CONF_PORT, + ENTITY_MATCH_ALL, +) +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_CAT, + CONF_DIM_STEPS, + CONF_FIRMWARE, + CONF_HOUSECODE, + CONF_HUB_PASSWORD, + CONF_HUB_USERNAME, + CONF_HUB_VERSION, + CONF_IP_PORT, + CONF_OVERRIDE, + CONF_PLM_HUB_MSG, + CONF_PRODUCT_KEY, + CONF_SUBCAT, + CONF_UNITCODE, + CONF_X10, + CONF_X10_ALL_LIGHTS_OFF, + CONF_X10_ALL_LIGHTS_ON, + CONF_X10_ALL_UNITS_OFF, + DOMAIN, + HOUSECODES, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_HOUSECODE, + SRV_LOAD_DB_RELOAD, + SRV_RESPONDER, +) + + +def set_default_port(schema: Dict) -> Dict: + """Set the default port based on the Hub version.""" + # If the ip_port is found do nothing + # If it is not found the set the default + ip_port = schema.get(CONF_IP_PORT) + if not ip_port: + hub_version = schema.get(CONF_HUB_VERSION) + # Found hub_version but not ip_port + if hub_version == 1: + schema[CONF_IP_PORT] = 9761 + else: + schema[CONF_IP_PORT] = 25105 + return schema + + +CONF_DEVICE_OVERRIDE_SCHEMA = vol.All( + cv.deprecated(CONF_PLATFORM), + vol.Schema( + { + vol.Required(CONF_ADDRESS): cv.string, + vol.Optional(CONF_CAT): cv.byte, + vol.Optional(CONF_SUBCAT): cv.byte, + vol.Optional(CONF_FIRMWARE): cv.byte, + vol.Optional(CONF_PRODUCT_KEY): cv.byte, + vol.Optional(CONF_PLATFORM): cv.string, + } + ), +) + + +CONF_X10_SCHEMA = vol.All( + vol.Schema( + { + vol.Required(CONF_HOUSECODE): cv.string, + vol.Required(CONF_UNITCODE): vol.Range(min=1, max=16), + vol.Required(CONF_PLATFORM): cv.string, + vol.Optional(CONF_DIM_STEPS): vol.Range(min=2, max=255), + } + ) +) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.All( + vol.Schema( + { + vol.Exclusive( + CONF_PORT, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Exclusive( + CONF_HOST, "plm_or_hub", msg=CONF_PLM_HUB_MSG + ): cv.string, + vol.Optional(CONF_IP_PORT): cv.port, + vol.Optional(CONF_HUB_USERNAME): cv.string, + vol.Optional(CONF_HUB_PASSWORD): cv.string, + vol.Optional(CONF_HUB_VERSION, default=2): vol.In([1, 2]), + 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] + ), + }, + extra=vol.ALLOW_EXTRA, + required=True, + ), + cv.has_at_least_one_key(CONF_PORT, CONF_HOST), + set_default_port, + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +ADD_ALL_LINK_SCHEMA = vol.Schema( + { + vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255), + vol.Required(SRV_ALL_LINK_MODE): vol.In([SRV_CONTROLLER, SRV_RESPONDER]), + } +) + + +DEL_ALL_LINK_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) + + +LOAD_ALDB_SCHEMA = vol.Schema( + { + vol.Required(CONF_ENTITY_ID): vol.Any(cv.entity_id, ENTITY_MATCH_ALL), + vol.Optional(SRV_LOAD_DB_RELOAD, default=False): cv.boolean, + } +) + + +PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id}) + + +X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)}) + + +TRIGGER_SCENE_SCHEMA = vol.Schema( + {vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)} +) diff --git a/homeassistant/components/insteon/sensor.py b/homeassistant/components/insteon/sensor.py index 0e8a592b92d..475723b105d 100644 --- a/homeassistant/components/insteon/sensor.py +++ b/homeassistant/components/insteon/sensor.py @@ -3,7 +3,7 @@ import logging from homeassistant.helpers.entity import Entity -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/switch.py b/homeassistant/components/insteon/switch.py index c36e60c2eff..eec7874c7fb 100644 --- a/homeassistant/components/insteon/switch.py +++ b/homeassistant/components/insteon/switch.py @@ -3,7 +3,7 @@ import logging from homeassistant.components.switch import SwitchDevice -from . import InsteonEntity +from .insteon_entity import InsteonEntity _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py new file mode 100644 index 00000000000..9a44566bb4a --- /dev/null +++ b/homeassistant/components/insteon/utils.py @@ -0,0 +1,239 @@ +"""Utilities used by insteon component.""" + +import logging + +from insteonplm.devices import ALDBStatus + +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 .const import ( + BUTTON_PRESSED_STATE_NAME, + DOMAIN, + EVENT_BUTTON_OFF, + EVENT_BUTTON_ON, + EVENT_CONF_BUTTON, + INSTEON_ENTITIES, + SIGNAL_LOAD_ALDB, + SIGNAL_PRINT_ALDB, + SRV_ADD_ALL_LINK, + SRV_ALL_LINK_GROUP, + SRV_ALL_LINK_MODE, + SRV_CONTROLLER, + SRV_DEL_ALL_LINK, + SRV_HOUSECODE, + SRV_LOAD_ALDB, + SRV_LOAD_DB_RELOAD, + SRV_PRINT_ALDB, + SRV_PRINT_IM_ALDB, + SRV_SCENE_OFF, + SRV_SCENE_ON, + SRV_X10_ALL_LIGHTS_OFF, + SRV_X10_ALL_LIGHTS_ON, + SRV_X10_ALL_UNITS_OFF, +) +from .ipdb import IPDB +from .schemas import ( + ADD_ALL_LINK_SCHEMA, + DEL_ALL_LINK_SCHEMA, + LOAD_ALDB_SCHEMA, + PRINT_ALDB_SCHEMA, + TRIGGER_SCENE_SCHEMA, + X10_HOUSECODE_SCHEMA, +) + +_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 + if val: + event = EVENT_BUTTON_ON + else: + event = EVENT_BUTTON_OFF + _LOGGER.debug( + "Firing event %s with address %s and button %s", event, address.hex, button + ) + hass.bus.fire(event, schema) + + @callback + def async_new_insteon_device(device): + """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 + + if platform == "on_off_events": + device.states[state_key].register_updates(_fire_button_on_off_event) + + else: + _LOGGER.info( + "New INSTEON device: %s (%s) %s", + device.address, + device.states[state_key].name, + platform, + ) + + 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) + + +@callback +def async_register_services(hass, config, insteon_modem): + """Register services used by insteon component.""" + + def 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) + + def 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) + + def 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) + else: + _send_load_aldb_signal(entity_id, 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) + + def print_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + entity_id = service.data[CONF_ENTITY_ID] + signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}" + dispatcher_send(hass, signal) + + def print_im_aldb(service): + """Print the All-Link Database for a device.""" + # For now this sends logs to the log file. + # Furture direction is to create an INSTEON control panel. + print_aldb_to_log(insteon_modem.aldb) + + def 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) + + def 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) + + def 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) + + def scene_on(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_on(group) + + def scene_off(service): + """Trigger an INSTEON scene ON.""" + group = service.data.get(SRV_ALL_LINK_GROUP) + insteon_modem.trigger_group_off(group) + + hass.services.async_register( + DOMAIN, SRV_ADD_ALL_LINK, 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 + ) + hass.services.async_register( + DOMAIN, SRV_LOAD_ALDB, 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, + ) + hass.services.async_register( + DOMAIN, SRV_X10_ALL_LIGHTS_OFF, 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, + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_ON, scene_on, schema=TRIGGER_SCENE_SCHEMA + ) + hass.services.async_register( + DOMAIN, SRV_SCENE_OFF, scene_off, schema=TRIGGER_SCENE_SCHEMA + ) + _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) + 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.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3") + _LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------") + for mem_addr in 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" + _LOGGER.info( + " {:04x} {:s} {:s} {:s} {:3d} {:s}" + " {:3d} {:3d} {:3d}".format( + rec.mem_addr, + in_use, + mode, + hwm, + rec.group, + rec.address.human, + rec.data1, + rec.data2, + rec.data3, + ) + )