Skip to content
Closed
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
296 changes: 273 additions & 23 deletions homeassistant/components/lovelace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,24 @@
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'
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we rename set to update, maybe it is clearer?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like update.

WS_TYPE_ADD_CARD = 'lovelace/config/card/add'
Copy link
Copy Markdown
Member

@balloob balloob Oct 22, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review would be a lot easier if you either have a single commit per WS command, so it's easy to review. Or if you do a PR per command.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do, tomorrow night I'll have time to work on it.

WS_TYPE_MOVE_CARD = 'lovelace/config/card/move'
WS_TYPE_MOVE_CARD_TO_VIEW = 'lovelace/config/card/view'
WS_TYPE_DELETE_CARD = 'lovelace/config/card/delete'

WS_TYPE_GET_VIEW = 'lovelace/config/view/get'
WS_TYPE_SET_VIEW = 'lovelace/config/view/set'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should not support this. People can move individual cards but shouldn't be able to do many cards at once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was that you would get the config of the view, without the cards, and could edit the name and icon, etc. But that was for a different PR, so should not have been in here...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. We should make a update command for view, but it should exclude cards then.

WS_TYPE_ADD_VIEW = 'lovelace/config/view/add'
WS_TYPE_MOVE_VIEW = 'lovelace/config/view/move'
WS_TYPE_DELETE_VIEW = 'lovelace/config/view/delete'

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 +45,43 @@
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,
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_SET_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),
})

SCHEMA_MOVE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_MOVE_CARD,
vol.Required('card_id'): str,
vol.Required('position'): int,
})

SCHEMA_MOVE_CARD_TO_VIEW = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_MOVE_CARD_TO_VIEW,
vol.Required('card_id'): str,
vol.Required('view_id'): str,
vol.Optional('position'): int,
})

SCHEMA_DELETE_CARD = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend({
vol.Required('type'): WS_TYPE_DELETE_CARD,
vol.Required('card_id'): str,
})


Expand All @@ -50,6 +93,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,32 +208,112 @@ 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 set_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:
cards.insert(position, card_config)
else:
cards.append(card_config)
return

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


def move_card(fname: str, card_id: str, position: int):
"""Move a card to a different position."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') != id:
continue
cards = view.get('cards')
cards.insert(position, cards.pop(cards.index(card)))
return

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


def move_card_to_view(fname: str, card_id: str, view_id: str,
position: int = None):
"""Move a card to a different view."""
config = load_yaml(fname)
for view in config.get('views', []):
if view.get('id') == view_id:
destination = view.get('cards')
for card in view.get('cards'):
if card.get('id') != id:
continue
origin = view.get('cards')
card_to_move = card

if 'destination' not in locals():
raise ViewNotFoundError(
"View with ID: {} was not found in {}.".format(view_id, fname))
if 'card_to_move' not in locals():
raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))

origin.pop(origin.index(card_to_move))
if position:
destination.insert(position, card_to_move)
else:
destination.append(card_to_move)
return


def delete_card(fname: str, card_id: str):
"""Delete card by id."""
config = load_yaml(fname)
for view in config.get('views', []):
for card in view.get('cards', []):
if card.get('id') == id:
cards = view.get('cards')
cards.pop(cards.index(card))
return

raise CardNotFoundError(
"Card with ID: {} was not found in {}.".format(card_id, fname))
Expand All @@ -211,6 +338,23 @@ async def async_setup(hass, config):
WS_TYPE_SET_CARD, websocket_lovelace_set_card,
SCHEMA_SET_CARD)

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

hass.components.websocket_api.async_register_command(
WS_TYPE_MOVE_CARD, websocket_lovelace_move_card,
SCHEMA_MOVE_CARD)

hass.components.websocket_api.async_register_command(
WS_TYPE_MOVE_CARD_TO_VIEW,
websocket_lovelace_move_card_to_view,
SCHEMA_MOVE_CARD_TO_VIEW)

hass.components.websocket_api.async_register_command(
WS_TYPE_DELETE_CARD, websocket_lovelace_delete_card,
SCHEMA_DELETE_CARD)

return True


Expand Down Expand Up @@ -245,7 +389,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 +398,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 @@ -273,7 +416,115 @@ async def websocket_lovelace_set_card(hass, connection, msg):
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'))
msg['card_id'], msg['card_config'], msg.get('format', FORMAT_YAML))
message = websocket_api.result_message(
msg['id'], result
)
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:
result = await hass.async_add_executor_job(
add_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['view_id'], msg['card_config'], msg.get('format', FORMAT_YAML))
message = websocket_api.result_message(
msg['id'], result
)
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 ViewNotFoundError as err:
error = 'view_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_move_card(hass, connection, msg):
"""Add new card to view over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
move_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['position'])
message = websocket_api.result_message(
msg['id'], result
)
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_move_card_to_view(hass, connection, msg):
"""Add new card to view over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
move_card_to_view, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'], msg['view_id'], msg.get('position'))
message = websocket_api.result_message(
msg['id'], result
)
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_delete_card(hass, connection, msg):
"""Add new card to view over websocket and save."""
error = None
try:
result = await hass.async_add_executor_job(
delete_card, hass.config.path(LOVELACE_CONFIG_FILE),
msg['card_id'])
message = websocket_api.result_message(
msg['id'], result
)
Expand All @@ -282,9 +533,8 @@ async def websocket_lovelace_set_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 = 'save_error', str(err)

Expand Down