2019-02-19 13:09:06 +00:00
|
|
|
"""Support for INSTEON Modems (PLM and Hub)."""
|
2018-02-25 19:13:39 +00:00
|
|
|
import collections
|
|
|
|
import logging
|
2018-09-10 14:54:17 +00:00
|
|
|
from typing import Dict
|
|
|
|
|
2017-02-21 07:53:39 +00:00
|
|
|
import voluptuous as vol
|
|
|
|
|
2019-02-19 13:09:06 +00:00
|
|
|
from homeassistant.const import (
|
2019-07-31 19:25:30 +00:00
|
|
|
CONF_ADDRESS,
|
|
|
|
CONF_ENTITY_ID,
|
|
|
|
CONF_HOST,
|
|
|
|
CONF_PLATFORM,
|
|
|
|
CONF_PORT,
|
|
|
|
ENTITY_MATCH_ALL,
|
|
|
|
EVENT_HOMEASSISTANT_STOP,
|
|
|
|
)
|
2018-12-14 13:02:06 +00:00
|
|
|
from homeassistant.core import callback
|
2017-02-21 07:53:39 +00:00
|
|
|
from homeassistant.helpers import discovery
|
2019-07-31 19:25:30 +00:00
|
|
|
from homeassistant.helpers.dispatcher import dispatcher_send, async_dispatcher_connect
|
2019-02-19 13:09:06 +00:00
|
|
|
import homeassistant.helpers.config_validation as cv
|
2018-02-25 19:13:39 +00:00
|
|
|
from homeassistant.helpers.entity import Entity
|
2017-02-21 07:53:39 +00:00
|
|
|
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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"
|
2018-07-18 14:11:54 +00:00
|
|
|
|
2018-09-10 14:54:17 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-02-25 19:13:39 +00:00
|
|
|
CONF_DEVICE_OVERRIDE_SCHEMA = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
),
|
|
|
|
)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2018-06-21 01:44:05 +00:00
|
|
|
CONF_X10_SCHEMA = vol.All(
|
2019-07-31 19:25:30 +00:00
|
|
|
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,
|
|
|
|
)
|
2018-08-22 07:09:04 +00:00
|
|
|
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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]),
|
|
|
|
}
|
|
|
|
)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DEL_ALL_LINK_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
|
|
|
|
)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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,
|
|
|
|
}
|
|
|
|
)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
PRINT_ALDB_SCHEMA = vol.Schema({vol.Required(CONF_ENTITY_ID): cv.entity_id})
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
X10_HOUSECODE_SCHEMA = vol.Schema({vol.Required(SRV_HOUSECODE): vol.In(HOUSECODES)})
|
2018-06-21 01:44:05 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
TRIGGER_SCENE_SCHEMA = vol.Schema(
|
|
|
|
{vol.Required(SRV_ALL_LINK_GROUP): vol.Range(min=0, max=255)}
|
|
|
|
)
|
2019-07-07 18:31:04 +00:00
|
|
|
|
|
|
|
|
2018-12-14 13:02:06 +00:00
|
|
|
STATE_NAME_LABEL_MAP = {
|
2019-07-31 19:25:30 +00:00
|
|
|
"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",
|
2018-12-14 13:02:06 +00:00
|
|
|
}
|
|
|
|
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-10-01 06:52:42 +00:00
|
|
|
async def async_setup(hass, config):
|
2018-08-22 07:09:04 +00:00
|
|
|
"""Set up the connection to the modem."""
|
2017-02-21 07:53:39 +00:00
|
|
|
import insteonplm
|
|
|
|
|
2018-02-25 19:13:39 +00:00
|
|
|
ipdb = IPDB()
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem = None
|
2018-02-25 19:13:39 +00:00
|
|
|
|
2017-02-21 07:53:39 +00:00
|
|
|
conf = config[DOMAIN]
|
|
|
|
port = conf.get(CONF_PORT)
|
2018-08-22 07:09:04 +00:00
|
|
|
host = conf.get(CONF_HOST)
|
|
|
|
ip_port = conf.get(CONF_IP_PORT)
|
|
|
|
username = conf.get(CONF_HUB_USERNAME)
|
|
|
|
password = conf.get(CONF_HUB_PASSWORD)
|
2018-09-10 14:54:17 +00:00
|
|
|
hub_version = conf.get(CONF_HUB_VERSION)
|
2018-02-25 19:13:39 +00:00
|
|
|
overrides = conf.get(CONF_OVERRIDE, [])
|
2018-06-21 01:44:05 +00:00
|
|
|
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)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
|
|
|
@callback
|
2018-08-22 07:09:04 +00:00
|
|
|
def async_new_insteon_device(device):
|
2017-04-30 05:04:49 +00:00
|
|
|
"""Detect device from transport to be delegated to platform."""
|
2018-02-25 19:13:39 +00:00
|
|
|
for state_key in device.states:
|
|
|
|
platform_info = ipdb[device.states[state_key]]
|
2018-07-18 14:11:54 +00:00
|
|
|
if platform_info and platform_info.platform:
|
2018-03-30 00:10:27 +00:00
|
|
|
platform = platform_info.platform
|
2018-07-18 14:11:54 +00:00
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
if platform == "on_off_events":
|
|
|
|
device.states[state_key].register_updates(_fire_button_on_off_event)
|
2018-07-18 14:11:54 +00:00
|
|
|
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info(
|
|
|
|
"New INSTEON device: %s (%s) %s",
|
|
|
|
device.address,
|
|
|
|
device.states[state_key].name,
|
|
|
|
platform,
|
|
|
|
)
|
2018-03-30 00:10:27 +00:00
|
|
|
|
2018-07-23 12:05:38 +00:00
|
|
|
hass.async_create_task(
|
2018-03-30 00:10:27 +00:00
|
|
|
discovery.async_load_platform(
|
2019-07-31 19:25:30 +00:00
|
|
|
hass,
|
|
|
|
platform,
|
|
|
|
DOMAIN,
|
|
|
|
discovered={
|
|
|
|
"address": device.address.id,
|
|
|
|
"state_key": state_key,
|
|
|
|
},
|
|
|
|
hass_config=config,
|
|
|
|
)
|
|
|
|
)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-05-05 15:15:20 +00:00
|
|
|
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
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.start_all_linking(link_mode, group)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
|
|
|
def del_all_link(service):
|
|
|
|
"""Delete an INSTEON All-Link between two devices."""
|
|
|
|
group = service.data.get(SRV_ALL_LINK_GROUP)
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.start_all_linking(255, group)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
|
|
|
def load_aldb(service):
|
|
|
|
"""Load the device All-Link database."""
|
2019-07-07 18:31:04 +00:00
|
|
|
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)
|
2018-05-05 15:15:20 +00:00
|
|
|
else:
|
2019-07-07 18:31:04 +00:00
|
|
|
_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."""
|
2019-09-03 15:27:14 +00:00
|
|
|
signal = f"{entity_id}_{SIGNAL_LOAD_ALDB}"
|
2019-07-07 18:31:04 +00:00
|
|
|
dispatcher_send(hass, signal, reload)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
|
|
|
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.
|
2019-07-07 18:31:04 +00:00
|
|
|
entity_id = service.data[CONF_ENTITY_ID]
|
2019-09-03 15:27:14 +00:00
|
|
|
signal = f"{entity_id}_{SIGNAL_PRINT_ALDB}"
|
2019-07-07 18:31:04 +00:00
|
|
|
dispatcher_send(hass, signal)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
|
|
|
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.
|
2018-08-22 07:09:04 +00:00
|
|
|
print_aldb_to_log(insteon_modem.aldb)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2018-06-21 01:44:05 +00:00
|
|
|
def x10_all_units_off(service):
|
|
|
|
"""Send the X10 All Units Off command."""
|
|
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.x10_all_units_off(housecode)
|
2018-06-21 01:44:05 +00:00
|
|
|
|
|
|
|
def x10_all_lights_off(service):
|
|
|
|
"""Send the X10 All Lights Off command."""
|
|
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.x10_all_lights_off(housecode)
|
2018-06-21 01:44:05 +00:00
|
|
|
|
|
|
|
def x10_all_lights_on(service):
|
|
|
|
"""Send the X10 All Lights On command."""
|
|
|
|
housecode = service.data.get(SRV_HOUSECODE)
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.x10_all_lights_on(housecode)
|
2018-06-21 01:44:05 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
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)
|
|
|
|
|
2018-05-05 15:15:20 +00:00
|
|
|
def _register_services():
|
2019-07-31 19:25:30 +00:00
|
|
|
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
|
|
|
|
)
|
2018-08-22 07:09:04 +00:00
|
|
|
_LOGGER.debug("Insteon Services registered")
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2018-07-18 14:11:54 +00:00
|
|
|
def _fire_button_on_off_event(address, group, val):
|
|
|
|
# Firing an event when a button is pressed.
|
2018-08-22 07:09:04 +00:00
|
|
|
device = insteon_modem.devices[address.hex]
|
2018-07-18 14:11:54 +00:00
|
|
|
state_name = device.states[group].name
|
2019-07-31 19:25:30 +00:00
|
|
|
button = (
|
|
|
|
"" if state_name == BUTTON_PRESSED_STATE_NAME else state_name[-1].lower()
|
|
|
|
)
|
2018-07-18 14:11:54 +00:00
|
|
|
schema = {CONF_ADDRESS: address.hex}
|
|
|
|
if button != "":
|
|
|
|
schema[EVENT_CONF_BUTTON] = button
|
|
|
|
if val:
|
|
|
|
event = EVENT_BUTTON_ON
|
|
|
|
else:
|
|
|
|
event = EVENT_BUTTON_OFF
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Firing event %s with address %s and button %s", event, address.hex, button
|
|
|
|
)
|
2018-07-18 14:11:54 +00:00
|
|
|
hass.bus.fire(event, schema)
|
|
|
|
|
2018-08-22 07:09:04 +00:00
|
|
|
if host:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info("Connecting to Insteon Hub on %s", host)
|
2018-10-01 06:52:42 +00:00
|
|
|
conn = await insteonplm.Connection.create(
|
2018-08-22 07:09:04 +00:00
|
|
|
host=host,
|
|
|
|
port=ip_port,
|
|
|
|
username=username,
|
|
|
|
password=password,
|
2018-09-10 14:54:17 +00:00
|
|
|
hub_version=hub_version,
|
2018-08-22 07:09:04 +00:00
|
|
|
loop=hass.loop,
|
2019-07-31 19:25:30 +00:00
|
|
|
workdir=hass.config.config_dir,
|
|
|
|
)
|
2018-08-22 07:09:04 +00:00
|
|
|
else:
|
|
|
|
_LOGGER.info("Looking for Insteon PLM on %s", port)
|
2018-10-01 06:52:42 +00:00
|
|
|
conn = await insteonplm.Connection.create(
|
2019-07-31 19:25:30 +00:00
|
|
|
device=port, loop=hass.loop, workdir=hass.config.config_dir
|
|
|
|
)
|
2018-08-22 07:09:04 +00:00
|
|
|
|
|
|
|
insteon_modem = conn.protocol
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-02-25 19:13:39 +00:00
|
|
|
for device_override in overrides:
|
2017-02-21 07:53:39 +00:00
|
|
|
#
|
|
|
|
# Override the device default capabilities for a specific address
|
|
|
|
#
|
2019-07-31 19:25:30 +00:00
|
|
|
address = device_override.get("address")
|
2018-02-25 19:13:39 +00:00
|
|
|
for prop in device_override:
|
|
|
|
if prop in [CONF_CAT, CONF_SUBCAT]:
|
2019-07-31 19:25:30 +00:00
|
|
|
insteon_modem.devices.add_override(address, prop, device_override[prop])
|
2018-02-25 19:13:39 +00:00
|
|
|
elif prop in [CONF_FIRMWARE, CONF_PRODUCT_KEY]:
|
2019-07-31 19:25:30 +00:00
|
|
|
insteon_modem.devices.add_override(
|
|
|
|
address, CONF_PRODUCT_KEY, device_override[prop]
|
|
|
|
)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-05-05 15:15:20 +00:00
|
|
|
hass.data[DOMAIN] = {}
|
2019-07-31 19:25:30 +00:00
|
|
|
hass.data[DOMAIN]["modem"] = insteon_modem
|
2019-07-07 18:31:04 +00:00
|
|
|
hass.data[DOMAIN][INSTEON_ENTITIES] = {}
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-02-25 19:13:39 +00:00
|
|
|
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, conn.close)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
2018-08-22 07:09:04 +00:00
|
|
|
insteon_modem.devices.add_device_callback(async_new_insteon_device)
|
2018-06-21 01:44:05 +00:00
|
|
|
|
|
|
|
if x10_all_units_off_housecode:
|
2019-07-31 19:25:30 +00:00
|
|
|
device = insteon_modem.add_x10_device(
|
|
|
|
x10_all_units_off_housecode, 20, "allunitsoff"
|
|
|
|
)
|
2018-06-21 01:44:05 +00:00
|
|
|
if x10_all_lights_on_housecode:
|
2019-07-31 19:25:30 +00:00
|
|
|
device = insteon_modem.add_x10_device(
|
|
|
|
x10_all_lights_on_housecode, 21, "alllightson"
|
|
|
|
)
|
2018-06-21 01:44:05 +00:00
|
|
|
if x10_all_lights_off_housecode:
|
2019-07-31 19:25:30 +00:00
|
|
|
device = insteon_modem.add_x10_device(
|
|
|
|
x10_all_lights_off_housecode, 22, "alllightsoff"
|
|
|
|
)
|
2018-06-21 01:44:05 +00:00
|
|
|
for device in x10_devices:
|
|
|
|
housecode = device.get(CONF_HOUSECODE)
|
|
|
|
unitcode = device.get(CONF_UNITCODE)
|
2019-07-31 19:25:30 +00:00
|
|
|
x10_type = "onoff"
|
2018-06-21 01:44:05 +00:00
|
|
|
steps = device.get(CONF_DIM_STEPS, 22)
|
2019-07-31 19:25:30 +00:00
|
|
|
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"):
|
2018-06-21 01:44:05 +00:00
|
|
|
device.states[0x01].steps = steps
|
|
|
|
|
2018-05-05 15:15:20 +00:00
|
|
|
hass.async_add_job(_register_services)
|
2017-02-21 07:53:39 +00:00
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
State = collections.namedtuple("Product", "stateType platform")
|
2018-02-25 19:13:39 +00:00
|
|
|
|
|
|
|
|
2018-07-20 08:45:20 +00:00
|
|
|
class IPDB:
|
2018-02-25 19:13:39 +00:00
|
|
|
"""Embodies the INSTEON Product Database static data and access methods."""
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
"""Create the INSTEON Product Database (IPDB)."""
|
2018-08-31 21:56:26 +00:00
|
|
|
from insteonplm.states.cover import Cover
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
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"),
|
|
|
|
]
|
2018-02-25 19:13:39 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
2018-08-22 07:09:04 +00:00
|
|
|
class InsteonEntity(Entity):
|
2018-02-25 19:13:39 +00:00
|
|
|
"""INSTEON abstract base entity."""
|
|
|
|
|
|
|
|
def __init__(self, device, state_key):
|
2018-08-22 07:09:04 +00:00
|
|
|
"""Initialize the INSTEON binary sensor."""
|
2018-02-25 19:13:39 +00:00
|
|
|
self._insteon_device_state = device.states[state_key]
|
|
|
|
self._insteon_device = device
|
2018-05-05 15:15:20 +00:00
|
|
|
self._insteon_device.aldb.add_loaded_callback(self._aldb_loaded)
|
2018-02-25 19:13:39 +00:00
|
|
|
|
|
|
|
@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
|
|
|
|
|
2018-10-07 11:12:33 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self) -> str:
|
|
|
|
"""Return a unique ID."""
|
|
|
|
if self._insteon_device_state.group == 0x01:
|
|
|
|
uid = self._insteon_device.id
|
|
|
|
else:
|
2019-07-31 19:25:30 +00:00
|
|
|
uid = "{:s}_{:d}".format(
|
|
|
|
self._insteon_device.id, self._insteon_device_state.group
|
|
|
|
)
|
2018-10-07 11:12:33 +00:00
|
|
|
return uid
|
|
|
|
|
2018-02-25 19:13:39 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Return the name of the node (used for Entity_ID)."""
|
2018-12-14 13:02:06 +00:00
|
|
|
# Set a base description
|
|
|
|
description = self._insteon_device.description
|
|
|
|
if self._insteon_device.description is None:
|
2019-07-31 19:25:30 +00:00
|
|
|
description = "Unknown Device"
|
2018-12-14 13:02:06 +00:00
|
|
|
|
|
|
|
# Get an extension label if there is one
|
|
|
|
extension = self._get_label()
|
|
|
|
if extension:
|
2019-07-31 19:25:30 +00:00
|
|
|
extension = " " + extension
|
|
|
|
name = "{:s} {:s}{:s}".format(
|
|
|
|
description, self._insteon_device.address.human, extension
|
2018-12-14 13:02:06 +00:00
|
|
|
)
|
2018-02-25 19:13:39 +00:00
|
|
|
return name
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self):
|
|
|
|
"""Provide attributes for display on device card."""
|
2019-07-31 19:25:30 +00:00
|
|
|
attributes = {"INSTEON Address": self.address, "INSTEON Group": self.group}
|
2018-02-25 19:13:39 +00:00
|
|
|
return attributes
|
|
|
|
|
|
|
|
@callback
|
2018-08-31 21:56:26 +00:00
|
|
|
def async_entity_update(self, deviceid, group, val):
|
2018-02-25 19:13:39 +00:00
|
|
|
"""Receive notification from transport that new data exists."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.debug(
|
|
|
|
"Received update for device %s group %d value %s",
|
|
|
|
deviceid.human,
|
|
|
|
group,
|
|
|
|
val,
|
|
|
|
)
|
2018-02-25 19:13:39 +00:00
|
|
|
self.async_schedule_update_ha_state()
|
|
|
|
|
2018-10-01 06:52:42 +00:00
|
|
|
async def async_added_to_hass(self):
|
2018-02-25 19:13:39 +00:00
|
|
|
"""Register INSTEON update events."""
|
2019-07-31 19:25:30 +00:00
|
|
|
_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)
|
2019-07-07 18:31:04 +00:00
|
|
|
self.hass.data[DOMAIN][INSTEON_ENTITIES][self.entity_id] = self
|
2019-09-03 15:27:14 +00:00
|
|
|
load_signal = f"{self.entity_id}_{SIGNAL_LOAD_ALDB}"
|
2019-07-07 18:31:04 +00:00
|
|
|
async_dispatcher_connect(self.hass, load_signal, self._load_aldb)
|
2019-09-03 15:27:14 +00:00
|
|
|
print_signal = f"{self.entity_id}_{SIGNAL_PRINT_ALDB}"
|
2019-07-07 18:31:04 +00:00
|
|
|
async_dispatcher_connect(self.hass, print_signal, self._print_aldb)
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
def _load_aldb(self, reload=False):
|
2018-05-05 15:15:20 +00:00
|
|
|
"""Load the device All-Link Database."""
|
|
|
|
if reload:
|
|
|
|
self._insteon_device.aldb.clear()
|
|
|
|
self._insteon_device.read_aldb()
|
|
|
|
|
2019-07-07 18:31:04 +00:00
|
|
|
def _print_aldb(self):
|
2018-05-05 15:15:20 +00:00
|
|
|
"""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."""
|
2019-07-07 18:31:04 +00:00
|
|
|
self._print_aldb()
|
2018-05-05 15:15:20 +00:00
|
|
|
|
2018-12-14 13:02:06 +00:00
|
|
|
def _get_label(self):
|
|
|
|
"""Get the device label for grouped devices."""
|
2019-07-31 19:25:30 +00:00
|
|
|
label = ""
|
2018-12-14 13:02:06 +00:00
|
|
|
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:
|
2019-09-03 15:27:14 +00:00
|
|
|
label = f"Group {self.group:d}"
|
2018-12-14 13:02:06 +00:00
|
|
|
return label
|
|
|
|
|
2018-05-05 15:15:20 +00:00
|
|
|
|
|
|
|
def print_aldb_to_log(aldb):
|
|
|
|
"""Print the All-Link Database to the log file."""
|
|
|
|
from insteonplm.devices import ALDBStatus
|
2019-07-31 19:25:30 +00:00
|
|
|
|
|
|
|
_LOGGER.info("ALDB load status is %s", aldb.status.name)
|
2018-05-05 15:15:20 +00:00
|
|
|
if aldb.status not in [ALDBStatus.LOADED, ALDBStatus.PARTIAL]:
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.warning("Device All-Link database not loaded")
|
|
|
|
_LOGGER.warning("Use service insteon.load_aldb first")
|
2018-05-05 15:15:20 +00:00
|
|
|
return
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
_LOGGER.info("RecID In Use Mode HWM Group Address Data 1 Data 2 Data 3")
|
|
|
|
_LOGGER.info("----- ------ ---- --- ----- -------- ------ ------ ------")
|
2018-05-05 15:15:20 +00:00
|
|
|
for mem_addr in aldb:
|
|
|
|
rec = aldb[mem_addr]
|
|
|
|
# For now we write this to the log
|
|
|
|
# Roadmap is to create a configuration panel
|
2019-07-31 19:25:30 +00:00
|
|
|
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,
|
|
|
|
)
|
|
|
|
)
|