core/homeassistant/components/elkm1/__init__.py

282 lines
9.8 KiB
Python

"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels."""
import logging
import re
import voluptuous as vol
from homeassistant.const import (
CONF_EXCLUDE, CONF_HOST, CONF_INCLUDE, CONF_PASSWORD,
CONF_TEMPERATURE_UNIT, CONF_USERNAME)
from homeassistant.core import HomeAssistant, callback # noqa
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import discovery
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import ConfigType # noqa
DOMAIN = 'elkm1'
CONF_AREA = 'area'
CONF_COUNTER = 'counter'
CONF_ENABLED = 'enabled'
CONF_KEYPAD = 'keypad'
CONF_OUTPUT = 'output'
CONF_PLC = 'plc'
CONF_SETTING = 'setting'
CONF_TASK = 'task'
CONF_THERMOSTAT = 'thermostat'
CONF_ZONE = 'zone'
CONF_PREFIX = 'prefix'
_LOGGER = logging.getLogger(__name__)
SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene',
'sensor', 'switch']
SPEAK_SERVICE_SCHEMA = vol.Schema({
vol.Required('number'):
vol.All(vol.Coerce(int), vol.Range(min=0, max=999)),
vol.Optional('prefix', default=''): cv.string
})
def _host_validator(config):
"""Validate that a host is properly configured."""
if config[CONF_HOST].startswith('elks://'):
if CONF_USERNAME not in config or CONF_PASSWORD not in config:
raise vol.Invalid("Specify username and password for elks://")
elif not config[CONF_HOST].startswith('elk://') and not config[
CONF_HOST].startswith('serial://'):
raise vol.Invalid("Invalid host URL")
return config
def _elk_range_validator(rng):
def _housecode_to_int(val):
match = re.search(r'^([a-p])(0[1-9]|1[0-6]|[1-9])$', val.lower())
if match:
return (ord(match.group(1)) - ord('a')) * 16 + int(match.group(2))
raise vol.Invalid("Invalid range")
def _elk_value(val):
return int(val) if val.isdigit() else _housecode_to_int(val)
vals = [s.strip() for s in str(rng).split('-')]
start = _elk_value(vals[0])
end = start if len(vals) == 1 else _elk_value(vals[1])
return (start, end)
def _has_all_unique_prefixes(value):
"""Validate that each m1 configured has a unique prefix.
Uniqueness is determined case-independently.
"""
prefixes = [device[CONF_PREFIX] for device in value]
schema = vol.Schema(vol.Unique())
schema(prefixes)
return value
DEVICE_SCHEMA_SUBDOMAIN = vol.Schema({
vol.Optional(CONF_ENABLED, default=True): cv.boolean,
vol.Optional(CONF_INCLUDE, default=[]): [_elk_range_validator],
vol.Optional(CONF_EXCLUDE, default=[]): [_elk_range_validator],
})
DEVICE_SCHEMA = vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_PREFIX, default=''): vol.All(cv.string, vol.Lower),
vol.Optional(CONF_USERNAME, default=''): cv.string,
vol.Optional(CONF_PASSWORD, default=''): cv.string,
vol.Optional(CONF_TEMPERATURE_UNIT, default='F'):
cv.temperature_unit,
vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_KEYPAD, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_OUTPUT, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_PLC, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_SETTING, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_TASK, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_THERMOSTAT, default={}): DEVICE_SCHEMA_SUBDOMAIN,
vol.Optional(CONF_ZONE, default={}): DEVICE_SCHEMA_SUBDOMAIN,
}, _host_validator)
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA],
_has_all_unique_prefixes)
}, extra=vol.ALLOW_EXTRA)
async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool:
"""Set up the Elk M1 platform."""
from elkm1_lib.const import Max
import elkm1_lib as elkm1
devices = {}
elk_datas = {}
configs = {
CONF_AREA: Max.AREAS.value,
CONF_COUNTER: Max.COUNTERS.value,
CONF_KEYPAD: Max.KEYPADS.value,
CONF_OUTPUT: Max.OUTPUTS.value,
CONF_PLC: Max.LIGHTS.value,
CONF_SETTING: Max.SETTINGS.value,
CONF_TASK: Max.TASKS.value,
CONF_THERMOSTAT: Max.THERMOSTATS.value,
CONF_ZONE: Max.ZONES.value,
}
def _included(ranges, set_to, values):
for rng in ranges:
if not rng[0] <= rng[1] <= len(values):
raise vol.Invalid("Invalid range {}".format(rng))
values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1)
for index, conf in enumerate(hass_config[DOMAIN]):
_LOGGER.debug("Setting up elkm1 #%d - %s", index, conf['host'])
config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]}
config['panel'] = {'enabled': True, 'included': [True]}
for item, max_ in configs.items():
config[item] = {'enabled': conf[item][CONF_ENABLED],
'included': [not conf[item]['include']] * max_}
try:
_included(conf[item]['include'], True,
config[item]['included'])
_included(conf[item]['exclude'], False,
config[item]['included'])
except (ValueError, vol.Invalid) as err:
_LOGGER.error("Config item: %s; %s", item, err)
return False
prefix = conf[CONF_PREFIX]
elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid':
conf[CONF_USERNAME],
'password': conf[CONF_PASSWORD]})
elk.connect()
devices[prefix] = elk
elk_datas[prefix] = {'elk': elk,
'prefix': prefix,
'config': config,
'keypads': {}}
_create_elk_services(hass, devices)
hass.data[DOMAIN] = elk_datas
for component in SUPPORTED_DOMAINS:
hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, {},
hass_config))
return True
def _create_elk_services(hass, elks):
def _speak_word_service(service):
prefix = service.data['prefix']
elk = elks.get(prefix)
if elk is None:
_LOGGER.error("No elk m1 with prefix for speak_word: '%s'",
prefix)
return
elk.panel.speak_word(service.data['number'])
def _speak_phrase_service(service):
prefix = service.data['prefix']
elk = elks.get(prefix)
if elk is None:
_LOGGER.error("No elk m1 with prefix for speak_phrase: '%s'",
prefix)
return
elk.panel.speak_phrase(service.data['number'])
hass.services.async_register(
DOMAIN, 'speak_word', _speak_word_service, SPEAK_SERVICE_SCHEMA)
hass.services.async_register(
DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA)
def create_elk_entities(elk_data, elk_elements, element_type,
class_, entities):
"""Create the ElkM1 devices of a particular class."""
if elk_data['config'][element_type]['enabled']:
elk = elk_data['elk']
_LOGGER.debug("Creating elk entities for %s", elk)
for element in elk_elements:
if elk_data['config'][element_type]['included'][element.index]:
entities.append(class_(element, elk, elk_data))
return entities
class ElkEntity(Entity):
"""Base class for all Elk entities."""
def __init__(self, element, elk, elk_data):
"""Initialize the base of all Elk devices."""
self._elk = elk
self._element = element
self._prefix = elk_data['prefix']
self._temperature_unit = elk_data['config']['temperature_unit']
# unique_id starts with elkm1_ iff there is no prefix
# it starts with elkm1m_{prefix} iff there is a prefix
# this is to avoid a conflict between
# prefix=foo, name=bar (which would be elkm1_foo_bar)
# - and -
# prefix="", name="foo bar" (which would be elkm1_foo_bar also)
# we could have used elkm1__foo_bar for the latter, but that
# would have been a breaking change
if self._prefix != "":
uid_start = 'elkm1m_{prefix}'.format(prefix=self._prefix)
else:
uid_start = 'elkm1'
self._unique_id = '{uid_start}_{name}'.format(
uid_start=uid_start,
name=self._element.default_name('_')).lower()
@property
def name(self):
"""Name of the element."""
return "{p}{n}".format(p=self._prefix, n=self._element.name)
@property
def unique_id(self):
"""Return unique id of the element."""
return self._unique_id
@property
def should_poll(self) -> bool:
"""Don't poll this device."""
return False
@property
def device_state_attributes(self):
"""Return the default attributes of the element."""
return {**self._element.as_dict(), **self.initial_attrs()}
@property
def available(self):
"""Is the entity available to be updated."""
return self._elk.is_connected()
def initial_attrs(self):
"""Return the underlying element's attributes as a dict."""
attrs = {}
attrs['index'] = self._element.index + 1
return attrs
def _element_changed(self, element, changeset):
pass
@callback
def _element_callback(self, element, changeset):
"""Handle callback from an Elk element that has changed."""
self._element_changed(element, changeset)
self.async_schedule_update_ha_state(True)
async def async_added_to_hass(self):
"""Register callback for ElkM1 changes and update entity state."""
self._element.add_callback(self._element_callback)
self._element_callback(self._element, {})