diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 564f0e74c75..f933228cc2f 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -24,6 +24,7 @@ CONF_SETTING = 'setting' CONF_TASK = 'task' CONF_THERMOSTAT = 'thermostat' CONF_ZONE = 'zone' +CONF_PREFIX = 'prefix' _LOGGER = logging.getLogger(__name__) @@ -32,7 +33,8 @@ SUPPORTED_DOMAINS = ['alarm_control_panel', 'climate', 'light', 'scene', SPEAK_SERVICE_SCHEMA = vol.Schema({ vol.Required('number'): - vol.All(vol.Coerce(int), vol.Range(min=0, max=999)) + vol.All(vol.Coerce(int), vol.Range(min=0, max=999)), + vol.Optional('prefix', default=''): cv.string }) @@ -63,32 +65,44 @@ def _elk_range_validator(rng): return (start, end) -CONFIG_SCHEMA_SUBDOMAIN = vol.Schema({ +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.Schema( - { - vol.Required(CONF_HOST): cv.string, - 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={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_COUNTER, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_KEYPAD, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_OUTPUT, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_PLC, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_SETTING, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_TASK, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_THERMOSTAT, default={}): CONFIG_SCHEMA_SUBDOMAIN, - vol.Optional(CONF_ZONE, default={}): CONFIG_SCHEMA_SUBDOMAIN, - }, - _host_validator, - ) + DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], + _has_all_unique_prefixes) }, extra=vol.ALLOW_EXTRA) @@ -97,6 +111,9 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: 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, @@ -115,27 +132,39 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: raise vol.Invalid("Invalid range {}".format(rng)) values[rng[0]-1:rng[1]] = [set_to] * (rng[1] - rng[0] + 1) - conf = hass_config[DOMAIN] - config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]} - config['panel'] = {'enabled': True, 'included': [True]} + for index, conf in enumerate(hass_config[DOMAIN]): + _LOGGER.debug("Setting up elkm1 #%d - %s", index, conf['host']) - 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 + config = {'temperature_unit': conf[CONF_TEMPERATURE_UNIT]} + config['panel'] = {'enabled': True, 'included': [True]} - elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': conf[CONF_USERNAME], - 'password': conf[CONF_PASSWORD]}) - elk.connect() + 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 - _create_elk_services(hass, elk) + prefix = conf[CONF_PREFIX] + elk = elkm1.Elk({'url': conf[CONF_HOST], 'userid': + conf[CONF_USERNAME], + 'password': conf[CONF_PASSWORD]}) + elk.connect() - hass.data[DOMAIN] = {'elk': elk, 'config': config, 'keypads': {}} + 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, {}, @@ -144,12 +173,24 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: return True -def _create_elk_services(hass, elk): +def _create_elk_services(hass, elks): def _speak_word_service(service): - elk.panel.speak_word(service.data.get('number')) + 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): - elk.panel.speak_phrase(service.data.get('number')) + 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) @@ -157,11 +198,12 @@ def _create_elk_services(hass, elk): DOMAIN, 'speak_phrase', _speak_phrase_service, SPEAK_SERVICE_SCHEMA) -def create_elk_entities(hass, elk_elements, element_type, class_, entities): +def create_elk_entities(elk_data, elk_elements, element_type, + class_, entities): """Create the ElkM1 devices of a particular class.""" - elk_data = hass.data[DOMAIN] 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)) @@ -175,14 +217,28 @@ class ElkEntity(Entity): """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'] - self._unique_id = 'elkm1_{}'.format( - self._element.default_name('_').lower()) + # 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 self._element.name + return "{p}{n}".format(p=self._prefix, n=self._element.name) @property def unique_id(self): diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index b885913a0df..5cf9107c39a 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -22,9 +22,11 @@ ELK_ALARM_SERVICE_SCHEMA = vol.Schema({ DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ENTITY_ID, default=[]): cv.entity_ids, - vol.Optional('clear', default=2): vol.In([0, 1, 2]), + vol.Optional('clear', default=2): vol.All(vol.Coerce(int), + vol.In([0, 1, 2])), vol.Optional('beep', default=False): cv.boolean, - vol.Optional('timeout', default=0): vol.Range(min=0, max=65535), + vol.Optional('timeout', default=0): vol.All(vol.Coerce(int), + vol.Range(min=0, max=65535)), vol.Optional('line1', default=''): cv.string, vol.Optional('line2', default=''): cv.string, }) @@ -36,8 +38,12 @@ async def async_setup_platform(hass, config, async_add_entities, if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.areas, 'area', ElkArea, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + entities = create_elk_entities(elk_data, elk.areas, + 'area', ElkArea, entities) async_add_entities(entities, True) def _dispatch(signal, entity_ids, *args): @@ -103,7 +109,7 @@ class ElkArea(ElkEntity, alarm.AlarmControlPanel): return if changeset.get('last_user') is not None: self._changed_by_entity_id = self.hass.data[ - ELK_DOMAIN]['keypads'].get(keypad.index, '') + ELK_DOMAIN][self._prefix]['keypads'].get(keypad.index, '') self.async_schedule_update_ha_state(True) @property diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index c3e9bcce860..36d44756857 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -21,9 +21,14 @@ async def async_setup_platform(hass, config, async_add_entities, if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities(create_elk_entities( - hass, elk.thermostats, 'thermostat', ElkThermostat, []), True) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + entities = create_elk_entities(elk_data, elk.thermostats, + 'thermostat', ElkThermostat, + entities) + async_add_entities(entities, True) class ElkThermostat(ElkEntity, ClimateDevice): diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index ee6fe09a7a2..50e91b091ee 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -10,9 +10,13 @@ async def async_setup_platform( """Set up the Elk light platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - async_add_entities( - create_elk_entities(hass, elk.lights, 'plc', ElkLight, []), True) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + create_elk_entities(elk_data, elk.lights, + 'plc', ElkLight, entities) + async_add_entities(entities, True) class ElkLight(ElkEntity, Light): diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 73b48623260..466f9da7e90 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -3,7 +3,7 @@ "name": "Elkm1", "documentation": "https://www.home-assistant.io/components/elkm1", "requirements": [ - "elkm1-lib==0.7.13" + "elkm1-lib==0.7.15" ], "dependencies": [], "codeowners": [] diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index aaae8bb0a5c..2133c0822e0 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -9,8 +9,12 @@ async def async_setup_platform( """Create the Elk-M1 scene platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.tasks, 'task', ElkTask, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + entities = create_elk_entities(elk_data, elk.tasks, + 'task', ElkTask, entities) async_add_entities(entities, True) diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index 0e367265605..f5811412cc8 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -8,17 +8,20 @@ async def async_setup_platform( if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities( - hass, elk.counters, 'counter', ElkCounter, []) - entities = create_elk_entities( - hass, elk.keypads, 'keypad', ElkKeypad, entities) - entities = create_elk_entities( - hass, [elk.panel], 'panel', ElkPanel, entities) - entities = create_elk_entities( - hass, elk.settings, 'setting', ElkSetting, entities) - entities = create_elk_entities( - hass, elk.zones, 'zone', ElkZone, entities) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + entities = create_elk_entities( + elk_data, elk.counters, 'counter', ElkCounter, entities) + entities = create_elk_entities( + elk_data, elk.keypads, 'keypad', ElkKeypad, entities) + entities = create_elk_entities( + elk_data, [elk.panel], 'panel', ElkPanel, entities) + entities = create_elk_entities( + elk_data, elk.settings, 'setting', ElkSetting, entities) + entities = create_elk_entities( + elk_data, elk.zones, 'zone', ElkZone, entities) async_add_entities(entities, True) @@ -92,8 +95,9 @@ class ElkKeypad(ElkSensor): async def async_added_to_hass(self): """Register callback for ElkM1 changes and update entity state.""" await super().async_added_to_hass() - self.hass.data[ELK_DOMAIN]['keypads'][ - self._element.index] = self.entity_id + elk_datas = self.hass.data[ELK_DOMAIN] + for elk_data in elk_datas.values(): + elk_data['keypads'][self._element.index] = self.entity_id class ElkPanel(ElkSensor): diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index df29491435e..2030ffe20ee 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -9,8 +9,12 @@ async def async_setup_platform( """Create the Elk-M1 switch platform.""" if discovery_info is None: return - elk = hass.data[ELK_DOMAIN]['elk'] - entities = create_elk_entities(hass, elk.outputs, 'output', ElkOutput, []) + elk_datas = hass.data[ELK_DOMAIN] + entities = [] + for elk_data in elk_datas.values(): + elk = elk_data['elk'] + entities = create_elk_entities(elk_data, elk.outputs, + 'output', ElkOutput, entities) async_add_entities(entities, True) diff --git a/requirements_all.txt b/requirements_all.txt index c5a2eee0a1d..44faddab03a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -424,7 +424,7 @@ eebrightbox==0.0.4 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.7.13 +elkm1-lib==0.7.15 # homeassistant.components.emulated_roku emulated_roku==0.1.8