Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 106 additions & 32 deletions homeassistant/components/lovelace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@
LOVELACE_CONFIG_FILE = 'ui-lovelace.yaml'
JSON_TYPE = Union[List, Dict, str] # pylint: disable=invalid-name

FORMAT_YAML = 'yaml'
FORMAT_JSON = 'json'

OLD_WS_TYPE_GET_LOVELACE_UI = 'frontend/lovelace_config'
WS_TYPE_GET_LOVELACE_UI = 'lovelace/config'

WS_TYPE_GET_CARD = 'lovelace/config/card/get'
WS_TYPE_SET_CARD = 'lovelace/config/card/set'
WS_TYPE_UPDATE_CARD = 'lovelace/config/card/update'
WS_TYPE_ADD_CARD = 'lovelace/config/card/add'

SCHEMA_GET_LOVELACE_UI = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): vol.Any(WS_TYPE_GET_LOVELACE_UI,
Expand All @@ -31,14 +36,25 @@
SCHEMA_GET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_GET_CARD,
vol.Required('card_id'): str,
vol.Optional('format', default='yaml'): str,
vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
FORMAT_YAML),
})

SCHEMA_SET_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_SET_CARD,
SCHEMA_UPDATE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_UPDATE_CARD,
vol.Required('card_id'): str,
vol.Required('card_config'): vol.Any(str, Dict),
vol.Optional('format', default='yaml'): str,
vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
FORMAT_YAML),
})

SCHEMA_ADD_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_ADD_CARD,
vol.Required('view_id'): str,
vol.Required('card_config'): vol.Any(str, Dict),
vol.Optional('position'): int,
vol.Optional('format', default=FORMAT_YAML): vol.Any(FORMAT_JSON,
FORMAT_YAML),
})


Expand All @@ -50,6 +66,10 @@ class CardNotFoundError(HomeAssistantError):
"""Card not found in data."""


class ViewNotFoundError(HomeAssistantError):
"""View not found in data."""


class UnsupportedYamlError(HomeAssistantError):
"""Unsupported YAML."""

Expand Down Expand Up @@ -161,37 +181,61 @@ def yaml_to_object(data: str) -> JSON_TYPE:
raise HomeAssistantError(exc)


def get_card(fname: str, card_id: str, data_format: str) -> JSON_TYPE:
def get_card(fname: str, card_id: str, data_format: str = FORMAT_YAML)\
-> JSON_TYPE:
"""Load a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
return object_to_yaml(card)
return card
if card.get('id') != card_id:
continue
if data_format == FORMAT_YAML:
return object_to_yaml(card)
return card

raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))


def set_card(fname: str, card_id: str, card_config: str, data_format: str)\
-> bool:
def update_card(fname: str, card_id: str, card_config: str,
data_format: str = FORMAT_YAML):
"""Save a specific card config for id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == card_id:
if data_format == 'yaml':
card_config = yaml_to_object(card_config)
card.update(card_config)
save_yaml(fname, config)
return True
if card.get('id') != card_id:
continue
if data_format == FORMAT_YAML:
card_config = yaml_to_object(card_config)
card.update(card_config)
save_yaml(fname, config)
return

raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))


def add_card(fname: str, view_id: str, card_config: str,
position: int = None, data_format: str = FORMAT_YAML):
"""Add a card to a view."""
config = load_yaml(fname)
for view in config.get('views', []):
if view.get('id') != view_id:
continue
cards = view.get('cards', [])
if data_format == FORMAT_YAML:
card_config = yaml_to_object(card_config)
if position is None:
cards.append(card_config)
else:
cards.insert(position, card_config)
save_yaml(fname, config)
return

raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname))


async def async_setup(hass, config):
"""Set up the Lovelace commands."""
# Backwards compat. Added in 0.80. Remove after 0.85
Expand All @@ -208,8 +252,12 @@ async def async_setup(hass, config):
SCHEMA_GET_CARD)

hass.components.websocket_api.async_register_command(
WS_TYPE_SET_CARD, websocket_lovelace_set_card,
SCHEMA_SET_CARD)
WS_TYPE_UPDATE_CARD, websocket_lovelace_update_card,
SCHEMA_UPDATE_CARD)

hass.components.websocket_api.async_register_command(
WS_TYPE_ADD_CARD, websocket_lovelace_add_card,
SCHEMA_ADD_CARD)

return True

Expand Down Expand Up @@ -245,7 +293,7 @@ async def websocket_lovelace_get_card(hass, connection, msg):
try:
card = await hass.async_add_executor_job(
get_card, hass.config.path(LOVELACE_CONFIG_FILE), msg['card_id'],
msg.get('format', 'yaml'))
msg.get('format', FORMAT_YAML))
message = websocket_api.result_message(
msg['id'], card
)
Expand All @@ -254,9 +302,8 @@ async def websocket_lovelace_get_card(hass, connection, msg):
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except CardNotFoundError as err:
error = 'card_not_found', str(err)
except HomeAssistantError as err:
error = 'load_error', str(err)

Expand All @@ -267,24 +314,51 @@ async def websocket_lovelace_get_card(hass, connection, msg):


@websocket_api.async_response
async def websocket_lovelace_set_card(hass, connection, msg):
async def websocket_lovelace_update_card(hass, connection, msg):
"""Receive lovelace card config over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
set_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['card_config'], msg.get('format', 'yaml'))
await hass.async_add_executor_job(
update_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML))
message = websocket_api.result_message(
msg['id'], True
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError as err:
error = 'card_not_found', str(err)
except HomeAssistantError as err:
error = 'save_error', str(err)

if error is not None:
message = websocket_api.error_message(msg['id'], *error)

connection.send_message(message)


@websocket_api.async_response
async def websocket_lovelace_add_card(hass, connection, msg):
"""Add new card to view over websocket and save."""
error = None
try:
await hass.async_add_executor_job(
add_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['view_id'], msg['card_config'], msg.get('position'),
msg.get('format', FORMAT_YAML))
message = websocket_api.result_message(
msg['id'], result
msg['id'], True
)
except FileNotFoundError:
error = ('file_not_found',
'Could not find ui-lovelace.yaml in your config dir.')
except UnsupportedYamlError as err:
error = 'unsupported_error', str(err)
except CardNotFoundError:
error = ('card_not_found',
'Could not find card in ui-lovelace.yaml.')
except ViewNotFoundError as err:
error = 'view_not_found', str(err)
except HomeAssistantError as err:
error = 'save_error', str(err)

Expand Down
71 changes: 62 additions & 9 deletions tests/components/lovelace/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,8 +370,8 @@ async def test_lovelace_get_card_bad_yaml(hass, hass_ws_client):
assert msg['error']['code'] == 'load_error'


async def test_lovelace_set_card(hass, hass_ws_client):
"""Test set_card command."""
async def test_lovelace_update_card(hass, hass_ws_client):
"""Test update_card command."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
Expand All @@ -382,7 +382,7 @@ async def test_lovelace_set_card(hass, hass_ws_client):
as save_yaml_mock:
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'type': 'lovelace/config/card/update',
'card_id': 'test',
'card_config': 'id: test\ntype: glance\n',
})
Expand All @@ -396,8 +396,8 @@ async def test_lovelace_set_card(hass, hass_ws_client):
assert msg['success']


async def test_lovelace_set_card_not_found(hass, hass_ws_client):
"""Test set_card command cannot find card."""
async def test_lovelace_update_card_not_found(hass, hass_ws_client):
"""Test update_card command cannot find card."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
Expand All @@ -406,7 +406,7 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client):
return_value=yaml.load(TEST_YAML_A)):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'type': 'lovelace/config/card/update',
'card_id': 'not_found',
'card_config': 'id: test\ntype: glance\n',
})
Expand All @@ -418,8 +418,8 @@ async def test_lovelace_set_card_not_found(hass, hass_ws_client):
assert msg['error']['code'] == 'card_not_found'


async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client):
"""Test set_card command bad yaml."""
async def test_lovelace_update_card_bad_yaml(hass, hass_ws_client):
"""Test update_card command bad yaml."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')
Expand All @@ -430,7 +430,7 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client):
side_effect=HomeAssistantError):
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/set',
'type': 'lovelace/config/card/update',
'card_id': 'test',
'card_config': 'id: test\ntype: glance\n',
})
Expand All @@ -440,3 +440,56 @@ async def test_lovelace_set_card_bad_yaml(hass, hass_ws_client):
assert msg['type'] == TYPE_RESULT
assert msg['success'] is False
assert msg['error']['code'] == 'save_error'


async def test_lovelace_add_card(hass, hass_ws_client):
"""Test add_card command."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')

with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)), \
patch('homeassistant.components.lovelace.save_yaml') \
as save_yaml_mock:
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/add',
'view_id': 'example',
'card_config': 'id: test\ntype: added\n',
})
msg = await client.receive_json()

result = save_yaml_mock.call_args_list[0][0][1]
assert result.mlget(['views', 0, 'cards', 2, 'type'],
list_ok=True) == 'added'
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success']


async def test_lovelace_add_card_position(hass, hass_ws_client):
"""Test add_card command."""
await async_setup_component(hass, 'lovelace')
client = await hass_ws_client(hass)
yaml = YAML(typ='rt')

with patch('homeassistant.components.lovelace.load_yaml',
return_value=yaml.load(TEST_YAML_A)), \
patch('homeassistant.components.lovelace.save_yaml') \
as save_yaml_mock:
await client.send_json({
'id': 5,
'type': 'lovelace/config/card/add',
'view_id': 'example',
'position': 0,
'card_config': 'id: test\ntype: added\n',
})
msg = await client.receive_json()

result = save_yaml_mock.call_args_list[0][0][1]
assert result.mlget(['views', 0, 'cards', 0, 'type'],
list_ok=True) == 'added'
assert msg['id'] == 5
assert msg['type'] == TYPE_RESULT
assert msg['success']