-
-
Notifications
You must be signed in to change notification settings - Fork 37.2k
[WIP] Add more functions to lovelace ws #17702
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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' | ||
| WS_TYPE_ADD_CARD = 'lovelace/config/card/add' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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...
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
|
|
@@ -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, | ||
| }) | ||
|
|
||
|
|
||
|
|
@@ -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.""" | ||
|
|
||
|
|
@@ -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)) | ||
|
|
@@ -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 | ||
|
|
||
|
|
||
|
|
@@ -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 | ||
| ) | ||
|
|
@@ -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) | ||
|
|
||
|
|
@@ -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 | ||
| ) | ||
|
|
@@ -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) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we rename
settoupdate, maybe it is clearer?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like update.