diff --git a/homeassistant/components/custom_card.py b/homeassistant/components/custom_card.py new file mode 100644 index 00000000000000..2b7fadd174cfc3 --- /dev/null +++ b/homeassistant/components/custom_card.py @@ -0,0 +1,105 @@ +""" +Show custom full-cards (ha-cards) and sate-cards on Home Assistant frontend. + +For more details about this component, please refer to the documentation +at https://home-assistant.io/components/custom_card/ +""" +import asyncio +import json +import logging + +import voluptuous as vol + +from homeassistant.components import http +from homeassistant.const import HTTP_BAD_REQUEST +import homeassistant.helpers.config_validation as cv + +DOMAIN = 'custom_card' +DEPENDENCIES = ['http'] + +_LOGGER = logging.getLogger(__name__) + +CONF_FULL_CARD = 'full_card' +CONF_STATE_CARD = 'state_card' +CONF_MORE_INFO_CARD = 'more_info_card' +CONF_CONFIG = 'config' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + cv.slug: vol.Any({ + vol.Optional(CONF_FULL_CARD): cv.string, + vol.Optional(CONF_STATE_CARD): cv.string, + vol.Optional(CONF_MORE_INFO_CARD): cv.string, + vol.Optional(CONF_CONFIG): cv.match_all, + }, None) + }) +}, extra=vol.ALLOW_EXTRA) + + +@asyncio.coroutine +def async_setup(hass, config): + """Initialize custom card.""" + card_configs = {} + + for object_id, cfg in config[DOMAIN].items(): + if not cfg: + cfg = {} + + full_card = cfg.get(CONF_FULL_CARD) + state_card = cfg.get(CONF_STATE_CARD) + more_info_card = cfg.get(CONF_MORE_INFO_CARD) + config = cfg.get(CONF_CONFIG) + + if full_card is None and state_card is None: + _LOGGER.error("Entity config must contain full_card " + + "and/or state_card (%s)", object_id) + return False + + if full_card: + entity_id = 'custom_full_card.{}'.format(object_id) + state = full_card + else: + entity_id = 'custom_state_card.{}'.format(object_id) + state = state_card + + attributes = {} + if full_card: + attributes['custom_ui_full_card'] = full_card + if state_card: + attributes['custom_ui_state_card'] = state_card + if more_info_card: + attributes['custom_ui_more_info_card'] = more_info_card + + if config: + card_configs[entity_id] = json.dumps(config) + + hass.states.async_set(entity_id, state, attributes) + + hass.http.register_view(CustomCardView(card_configs)) + + return True + + +class CustomCardView(http.HomeAssistantView): + """API to request card config.""" + + url = '/api/custom_card' + name = 'api:custom_card' + + def __init__(self, card_configs): + """Initialize a custom card view.""" + self._card_configs = card_configs + + @http.RequestDataValidator(vol.Schema({ + vol.Required('entity_id'): str, + })) + @asyncio.coroutine + def post(self, request, data): + """Handle a config request.""" + try: + config = self._card_configs[data['entity_id']] + except KeyError: + return self.json_message('For this entity is no config available.', + HTTP_BAD_REQUEST) + else: + return self.json({'config': config}) diff --git a/tests/components/test_custom_card.py b/tests/components/test_custom_card.py new file mode 100644 index 00000000000000..8c6edd4bcc9337 --- /dev/null +++ b/tests/components/test_custom_card.py @@ -0,0 +1,83 @@ +"""The tests the custom cards component.""" +# pylint: disable=protected-access +import unittest +import logging + +from homeassistant.setup import setup_component +from homeassistant.components.custom_card import DOMAIN + +from tests.common import get_test_home_assistant + +_LOGGER = logging.getLogger(__name__) + + +class TestCustomCard(unittest.TestCase): + """Test the custom card module.""" + + # pylint: disable=invalid-name + def setUp(self): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + # pylint: disable=invalid-name + def tearDown(self): + """Stop everything that was started.""" + self.hass.stop() + + def test_config(self): + """Test config.""" + invalid_configs = [ + None, + 1, + {}, + {'name with space': None}, + {'noCardsDefined': None}, + {'moreInfoOnly': {'more_info_card': 'test-card'}}, + ] + + for cfg in invalid_configs: + self.assertFalse( + setup_component(self.hass, DOMAIN, {DOMAIN: cfg})) + + def test_config_options(self): + """Test configuration options.""" + count_start = len(self.hass.states.entity_ids()) + + _LOGGER.debug('ENTITIES @ start: %s', self.hass.states.entity_ids()) + + self.assertTrue(setup_component(self.hass, DOMAIN, {DOMAIN: { + 'test_1': { + 'full_card': 'full-card', + }, + 'test_2': { + 'state_card': 'state-card', + }, + 'test_3': { + 'full_card': 'full-card', + 'state_card': 'state-card', + 'more_info_card': 'more-info-card', + }, + }})) + + _LOGGER.debug('ENTITIES: %s', self.hass.states.entity_ids()) + + self.assertEqual(count_start + 3, len(self.hass.states.entity_ids())) + + state_1 = self.hass.states.get('custom_full_card.test_1') + state_2 = self.hass.states.get('custom_state_card.test_2') + state_3 = self.hass.states.get('custom_full_card.test_3') + + self.assertIsNotNone(state_1) + self.assertIsNotNone(state_2) + self.assertIsNotNone(state_3) + + self.assertEqual('full-card', state_1.state) + self.assertEqual('state-card', state_2.state) + self.assertEqual('full-card', state_3.state) + + self.assertEqual('full-card', + state_3.attributes.get('custom_ui_full_card')) + self.assertEqual('state-card', + state_3.attributes.get('custom_ui_state_card')) + self.assertEqual('more-info-card', + state_3.attributes.get('custom_ui_more_info_card'))