1
mirror of https://github.com/home-assistant/core synced 2024-10-04 07:58:43 +02:00
ha-core/homeassistant/components/insteon/__init__.py
Brian Towles a30921e67d Set InsteonEntity name to be combo of description and address. (#17262)
* Set InsteonEntity name to be combo of description and address.

ie "LampLinc Dimmer 26453a" or "Keypad Dimmer 291abb_2"

Using a centralized network name

* Updated the name to have a more contextual references for device
functions then just the group id.

* Cleanup for hound

* Removed the _generate_network_address function.  Not used anymore

* Cleanup for lint

* clean for hound

* Moved descriptor mapper to be a class variable of the InsteonEntity in order to fix lib loading issue for lint

* Well, moved DescriptorMapper instance to a function variable now...

* fix hound

* Lint Cleanup

* Clean up docstrings

* Simplify Label lookup based on state name
2018-12-14 14:02:06 +01:00

610 lines
23 KiB
Python

"""
Support for INSTEON Modems (PLM and Hub).
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/insteon/
"""
import collections
import logging
from typing import Dict
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.const import (CONF_PORT, EVENT_HOMEASSISTANT_STOP,
CONF_PLATFORM,
CONF_ENTITY_ID,
CONF_HOST)
from homeassistant.core import callback
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
REQUIREMENTS = ['insteonplm==0.15.2']
_LOGGER = logging.getLogger(__name__)
DOMAIN = 'insteon'
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_ADDRESS = 'address'
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'
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): cv.entity_id,
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),
})
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."""
import insteonplm
ipdb = IPDB()
insteon_modem = None
conf = config[DOMAIN]
port = conf.get(CONF_PORT)
host = conf.get(CONF_HOST)
ip_port = conf.get(CONF_IP_PORT)
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)
@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.get(CONF_ENTITY_ID)
reload = service.data.get(SRV_LOAD_DB_RELOAD)
entities = hass.data[DOMAIN].get('entities')
entity = entities.get(entity_id)
if entity:
entity.load_aldb(reload)
else:
_LOGGER.error('Entity %s is not an INSTEON device', entity_id)
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.get(CONF_ENTITY_ID)
entities = hass.data[DOMAIN].get('entities')
entity = entities.get(entity_id)
if entity:
entity.print_aldb()
else:
_LOGGER.error('Entity %s is not an INSTEON device', entity_id)
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 _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)
_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(
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)
insteon_modem = conn.protocol
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])
hass.data[DOMAIN] = {}
hass.data[DOMAIN]['modem'] = insteon_modem
hass.data[DOMAIN]['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')
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'
steps = device.get(CONF_DIM_STEPS, 22)
if device.get(CONF_PLATFORM) == 'light':
x10_type = 'dimmable'
elif device.get(CONF_PLATFORM) == 'binary_sensor':
x10_type = 'sensor'
_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
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)."""
from insteonplm.states.cover import Cover
from insteonplm.states.onOff import (OnOffSwitch,
OnOffSwitch_OutletTop,
OnOffSwitch_OutletBottom,
OpenClosedRelay,
OnOffKeypadA,
OnOffKeypad)
from insteonplm.states.dimmable import (DimmableSwitch,
DimmableSwitch_Fan,
DimmableRemote,
DimmableKeypadA)
from insteonplm.states.sensor import (VariableSensor,
OnOffSensor,
SmokeCO2Sensor,
IoLincSensor,
LeakSensorDryWet)
from insteonplm.states.x10 import (X10DimmableSwitch,
X10OnOffSwitch,
X10OnOffSensor,
X10AllUnitsOffSensor,
X10AllLightsOnSensor,
X10AllLightsOffSensor)
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 = ' ' + 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]['entities'][self.entity_id] = self
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 = 'Group {:d}'.format(self.group)
return label
def print_aldb_to_log(aldb):
"""Print the All-Link Database to the log file."""
from insteonplm.devices import ALDBStatus
_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))