diff --git a/hass/settings/custom_components/hacs.zip b/hass/settings/custom_components/hacs.zip deleted file mode 100644 index f2de9e5..0000000 Binary files a/hass/settings/custom_components/hacs.zip and /dev/null differ diff --git a/hass/settings/lovelace_migrate.py b/hass/settings/lovelace_migrate.py deleted file mode 100644 index 003a469..0000000 --- a/hass/settings/lovelace_migrate.py +++ /dev/null @@ -1,844 +0,0 @@ -""" -Migration tool for Home Assistant Lovelace UI. -""" -import argparse -import logging -import sys -import json -import os -import shutil - -from collections import OrderedDict -from getpass import getpass - -import requests -import yaml - -_LOGGER = logging.getLogger(__name__) - - -# Build arguments parser (argdown needs this at the beginning of the file) -parser = argparse.ArgumentParser( - description="Home Assistant Lovelace migration tool") - -# Positional arguments -parser.add_argument( - 'input', metavar='', nargs='?', - help="Home Assistant REST API URL or states JSON file") - -# Optional arguments -parser.add_argument( - '-o', '--output', metavar='', default='ui-lovelace.yaml', - help="write output to (default: ui-lovelace.yaml)") -parser.add_argument( - '-p', '--password', metavar='', nargs='?', - default=False, const=None, - help="Home Assistant API password") -parser.add_argument( - '-t', '--title', metavar='', default='Home', - help="title of the Lovelace UI (default: Home)") -parser.add_argument( - '--debug', action='store_true', - help="set log level to DEBUG") -parser.add_argument( - '--dry-run', action='store_true', - help="do not write to output file") - -# Parse the args -args = parser.parse_args() - -# Input was not provided, so we need to check a few other things -if args.input is None: - if args.password: - # User expects a password prompt - args.input = args.password - args.password = None - elif os.getenv('HASSIO_TOKEN') is not None: - # Script is running in Hass.io environment - args.input = 'http://hassio/homeassistant/api' - args.password = os.getenv('HASSIO_TOKEN') - else: - # Other defaults were not found - args.input = 'http://localhost:8123/api' - - -def dd(msg=None, j=None, *args): - if j is None and len(args) == 0: - j = msg - msg = "{}" - if j is not None: - _LOGGER.debug(msg.format(json.dumps(j, indent=2))) - else: - _LOGGER.debug(msg.format(*args)) - - -class LovelaceBase(OrderedDict): - """ - Base class for Lovelace objects. - - Derivitives should set `key_order`: - - self.key_order = ['first', 'second', '...', 'last'] - """ - - def __init__(self, **kwargs): - """Initialize the object.""" - self.update(kwargs) - for key, value in list(self.items()): - if value is None: - del self[key] - - def __setitem__(self, key, value): - sort = key not in self.keys() - super().__setitem__(key, value) - if sort: - self.sortkeys() - - @classmethod - def from_config(cls, config): - """ - Subclass should implement config conversion methods `from_xxx_config`: - - from_camera_config(cls, config) - from_media_player_config(cls, config) - from_group_config(cls, config) - """ - - def invalid_config(cls, config={}, exception=None): - """Display an error about invalid config.""" - _LOGGER.error("Invalid config for conversion to '{}': {}" - "".format(cls.__name__, exception)) - if config is not None: - output = json.dumps(config, indent=2) - else: - output = config - _LOGGER.debug("Invalid config: {}".format(output)) - - if 'entity_id' not in config: - invalid_config(cls, config, "Config is missing 'entity_id'") - return None - - entity_id = config['entity_id'] - domain, object_id = entity_id.split('.', 1) - - fx = getattr(cls, "from_" + domain + "_config", None) - if fx is None: - _LOGGER.error("Class '{}' does not support conversion from " - "'{}' config".format(cls.__name__, domain)) - return None - return fx(config) - - def add_item(self, key, item): - """Add item(s) to the object.""" - if item is not None: - if key not in self.keys(): - self[key] = [] - if type(item) is list: - self[key].extend(item) - else: - self[key].append(item) - - def sortkeys(self, key_order=None, delim='...'): - """Iterate keys of OrderedDict and move to front/back as necessary.""" - # Get `keys` from self, but fallback on parent - if key_order is None: - try: - key_order = self.key_order - except AttributeError: - try: - key_order = super(OrderedDict, self).key_order - except AttributeError: - pass - - if key_order is None: - return - - # Make a copy so that we're not changing the original - key_order = key_order[:] - - # Check to see if delimiter is in `key_order` - if delim in key_order: - mid = key_order.index(delim) - else: - mid = len(key_order) - - # Reverse the front keys - key_order[:mid] = key_order[:mid][::-1] - - # Iterate keys and move them accordingly - for i, key in enumerate(key_order): - # Skip delimiter and missing keys - if i == mid or key not in self: - continue - - # Move to front/back - self.move_to_end(key, last=i > mid) - - -class Lovelace(LovelaceBase): - """Lovelace migration class.""" - - class View(LovelaceBase): - """Lovelace UI view representation.""" - - def __init__(self, **kwargs): - """Init view.""" - self.key_order = ['title', 'id', 'icon', 'panel', 'theme', '...', - 'cards'] - super().__init__(**kwargs) - - def add_card(self, card): - """Add a card to the view.""" - return self.add_item('cards', card) - - @classmethod - def from_group_config(cls, group): - """Build the view from `group` config.""" - if not group['attributes'].get('view', False): - return None - - view = cls(title=group['attributes'].get('friendly_name'), - icon=group['attributes'].get('icon')) - cards, nocards = [], [] - - for entity in group.get('entities', {}).values(): - card = Lovelace.Card.from_config(entity) - if type(card) is list: - cards.extend(card) - elif card is not None: - cards.append(card) - else: - nocards.append(entity['entity_id']) - - if len(nocards): - cards = [Lovelace.EntitiesCard(entities=nocards)] + cards - - view.add_card(cards) - return view - - class Card(LovelaceBase): - """Lovelace UI card representation.""" - - @classmethod - def from_config(cls, config): - """Convert a config object to Lovelace UI.""" - if config is None: - return None - - if cls is not Lovelace.Card: - return super().from_config(config) - - domain = config['domain'] - if domain in Lovelace.CARD_DOMAINS: - cls = Lovelace.CARD_DOMAINS[domain] - return cls.from_config(config) - - return None - - # @todo Implement use of this in `add_entity` - class Entity(LovelaceBase): - """Lovelace UI entity representation.""" - - def __init__(self, **kwargs): - """Init entity.""" - self.key_order = ['entity', 'name'] - super().__init__(**kwargs) - - class Resource(LovelaceBase): - """Lovelace UI resource representation.""" - - def __init__(self, **kwargs): - """Init resource.""" - self.key_order = ['url', 'type'] - kwargs.setdefault('type', 'js') - super().__init__(**kwargs) - - class EntitiesCard(Card): - """Lovelove UI `entities` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'entities' - self.key_order = ['type', 'title', 'show_header_toggle', '...', - 'entities'] - super().__init__(**kwargs) - - def add_entity(self, entity): - """Add an entity to the card.""" - return self.add_item('entities', entity) - - @classmethod - def from_group_config(cls, group): - """Build the card from `group` config.""" - control = group['attributes'].get('control') != 'hidden' - cards, nocards = [], [] - - for entity in group.get('entities', {}).values(): - card = Lovelace.Card.from_config(entity) - if type(card) is list: - cards.extend(card) - elif card is not None: - cards.append(card) - else: - nocards.append(entity['entity_id']) - - if len(nocards): - primary = cls(title=group['attributes'].get('friendly_name'), - show_header_toggle=control, - entities=nocards) - return [primary] + cards - - return cards - - class EntityFilterCard(Card): - """Lovelove UI `entity-filter` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'entity-filter' - self.key_order = ['type', 'entities', 'state_filter', 'card', - 'show_empty'] - super().__init__(**kwargs) - - def add_entity(self, entity): - """Add an entity to the card.""" - return self.add_item('entities', entity) - - def add_state_filter(self, state_filter): - """Add a state filter to the card.""" - return self.add_item('state_filter', state_filter) - - class GlanceCard(Card): - """Lovelove UI `glance` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'glance' - self.key_order = ['type', 'title', '...', 'entities'] - super().__init__(**kwargs) - - def add_entity(self, entity): - """Add an entity to the card.""" - return self.add_item('entities', entity) - - class HistoryGraphCard(Card): - """Lovelove UI `history-graph` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'history-graph' - self.key_order = ['type', 'title', 'hours_to_show', - 'refresh_interval', '...', 'entities'] - super().__init__(**kwargs) - - def add_entity(self, entity): - """Add an entity to the card.""" - return self.add_item('entities', entity) - - @classmethod - def from_history_graph_config(cls, config): - """Build the card from `history_graph` config.""" - return cls(title=config['attributes'].get('friendly_name'), - hours_to_show=config['attributes'].get('hours_to_show'), - refresh_interval=config['attributes'].get('refresh'), - entities=config['attributes']['entity_id']) - - class HorizontalStackCard(Card): - """Lovelove UI `horizontal-stack` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'horizontal-stack' - self.key_order = ['type', '...', 'cards'] - super().__init__(**kwargs) - - def add_card(self, card): - """Add a card to the card.""" - return self.add_item('cards', card) - - class IframeCard(Card): - """Lovelove UI `iframe` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'iframe' - self.key_order = ['type', 'title', 'url', 'aspect_ratio'] - super().__init__(**kwargs) - - class MapCard(Card): - """Lovelove UI `map` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'map' - self.key_order = ['type', 'title', 'aspect_ratio', '...', - 'entities'] - super().__init__(**kwargs) - - def add_entity(self, entity): - """Add an entity to the card.""" - return self.add_item('entities', entity) - - class MarkdownCard(Card): - """Lovelove UI `markdown` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'markdown' - self.key_order = ['type', 'title', '...', 'content'] - super().__init__(**kwargs) - - class MediaControlCard(Card): - """Lovelove UI `media-control` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'media-control' - self.key_order = ['type', 'entity'] - super().__init__(**kwargs) - - @classmethod - def from_media_player_config(cls, config): - """Build the card from `media_player` config.""" - return cls(entity=config['entity_id']) - - class PictureCard(Card): - """Lovelove UI `picture` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'picture' - self.key_order = ['type', 'image', 'navigation_path', 'service', - 'service_data'] - super().__init__(**kwargs) - - class PictureElementsCard(Card): - """Lovelove UI `picture-elements` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'picture-elements' - self.key_order = ['type', 'title', 'image', 'elements'] - super().__init__(**kwargs) - - def add_element(self, element): - """Add an element to the card.""" - return self.add_item('elements', element) - - class PictureEntityCard(Card): - """Lovelove UI `picture-entity` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'picture-entity' - self.key_order = ['type', 'title', 'entity', 'camera_image', - 'image', 'state_image', 'show_info', - 'tap_action'] - super().__init__(**kwargs) - - @classmethod - def from_camera_config(cls, config): - """Build the card from `camera` config.""" - return cls(title=config['attributes'].get('friendly_name'), - entity=config['entity_id'], - camera_image=config['entity_id'], - show_info=True, - tap_action='dialog') - - class PictureGlanceCard(Card): - """Lovelove UI `picture-glance` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'picture-glance' - self.key_order = ['type', 'title', '...', 'entities'] - super().__init__(**kwargs) - - class PlantStatusCard(Card): - """Lovelove UI `plant-status` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'plant-status' - self.key_order = ['type', 'entity'] - super().__init__(**kwargs) - - @classmethod - def from_plant_config(cls, config): - """Build the card from `plant` config.""" - return cls(entity=config['entity_id']) - - class VerticalStackCard(Card): - """Lovelove UI `vertical-stack` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'vertical-stack' - self.key_order = ['type', '...', 'cards'] - super().__init__(**kwargs) - - def add_card(self, card): - """Add a card to the card.""" - return self.add_item('cards', card) - - class WeatherForecastCard(Card): - """Lovelove UI `weather-forecast` card representation.""" - - def __init__(self, **kwargs): - """Init card.""" - self['type'] = 'weather-forecast' - self.key_order = ['type', 'entity'] - super().__init__(**kwargs) - - @classmethod - def from_weather_config(cls, config): - """Build the card from `weather` config.""" - return cls(entity=config['entity_id']) - - class CustomCard(Card): - """Lovelace UI `custom` card representation.""" - - def __init__(self, card_type, resource=None, key_order=None, **kwargs): - """Init card.""" - if card_type in Lovelace.CUSTOM_CARDS: - custom = Lovelace.CUSTOM_CARDS[card_type] - if resource is None and 'resource' in custom: - resource = custom['resource'] - if key_order is None and 'key_order' in custom: - key_order = custom['key_order'] - - self['type'] = 'custom:' + card_type - self.key_order = key_order or ['type', '...'] - self.resource = resource - super().__init__(**kwargs) - - # @todo Possibly move this into CARD_DOMAINS and `from_config` - # AUTO_DOMAINS = { - # 'all_lights': 'light', - # 'all_automations': 'automation', - # 'all_devices': 'device_tracker', - # 'all_fans': 'fan', - # 'all_locks': 'lock', - # 'all_covers': 'cover', - # 'all_remotes': 'remote', - # 'all_switches': 'switch', - # 'all_vacuum_cleaners': 'vacuum', - # 'all_scripts': 'script', - # } - - CARD_DOMAINS = { - 'camera': PictureEntityCard, - 'group': EntitiesCard, - 'history_graph': HistoryGraphCard, - 'media_player': MediaControlCard, - 'plant': PlantStatusCard, - 'weather': WeatherForecastCard, - } - - CUSTOM_CARDS = { - 'monster-card': { - 'resource': 'https://cdn.rawgit.com/ciotlosm/custom-lovelace/c9465a72a2f484fce135dce86c35412f099d493f/monster-card/monster-card.js', - 'key_order': ['type', 'card', 'filter', 'when', '...'] - }, - } - - def __init__(self, states_json, title=None): - """Convert existing Home Assistant config to Lovelace UI.""" - self.key_order = ['title', 'resources', 'excluded_entities', - '...', 'views'] - super().__init__() - - self['title'] = title or "Home" - - # Build states and entities objects from the states JSON - self._states = states = self.build_states(states_json) - - groups = states.get('group', {}) - views = {k: v for k, v in groups.items() - if v['attributes'].get('view', False)} - - if 'default_view' in views: - self.add_view(Lovelace.View.from_config( - views.pop('default_view'))) - else: - view = Lovelace.View(title='Home') - - for domain in Lovelace.CARD_DOMAINS.keys(): - for e in states.get(domain, {}).values(): - if (domain == 'group' and - e['attributes'].get('view', False)): - continue - - card = Lovelace.Card.from_config(e) - if card is not None: - view.add_card(card) - - if view.get('cards') is not None: - self.add_view(view) - - for view in views.values(): - self.add_view(Lovelace.View.from_config(view)) - - def add_resource(self, resource): - """Add a resource to the UI.""" - if type(resource) is str: - resource = Lovelace.Resource(url=resource) - elif type(resource) is dict: - resource = Lovelace.Resource(resource) - return self.add_item('resources', resource) - - def add_view(self, view): - """Add a view to the UI.""" - return self.add_item('views', view) - - def build_states(self, states_json): - """Build a states object from states JSON.""" - all_entities = self.build_entities(states_json) - states = {} - - for e in all_entities.values(): - if 'entity_id' in e['attributes']: - e['entities'] = {} - for x in e['attributes']['entity_id']: - if x in all_entities: - e['entities'].update({ - x: all_entities[x] - }) - - if e['domain'] not in states: - states[e['domain']] = {} - - states[e['domain']].update({ - e['object_id']: e - }) - - return states - - def build_entities(self, states_json): - """Build a list of entities from states JSON.""" - entities = {} - - for e in states_json: - # Add domain and object_id - e['domain'], e['object_id'] = e['entity_id'].split('.', 1) - - # Add name from `friendly_name` or build from `object_id` - e['attributes']['friendly_name'] = e['attributes'].get( - 'friendly_name', e['object_id'].replace('_', ' ').title()) - - # Add entity to the entities object - entities[e['entity_id']] = e - - return entities - - def dump(self): - """Dump YAML for the Lovelace UI.""" - def ordered_dump(data, stream=None, Dumper=yaml.Dumper, **kwargs): - """YAML dumper for OrderedDict.""" - - class OrderedDumper(Dumper): - """Wrapper class for YAML dumper.""" - - def ignore_aliases(self, data): - """Disable aliases in YAML dump.""" - return True - - def increase_indent(self, flow=False, indentless=False): - """Increase indent on YAML lists.""" - return super(OrderedDumper, self).increase_indent( - flow, False) - - def _dict_representer(dumper, data): - """Function to represent OrderDict and derivitives.""" - return dumper.represent_mapping( - yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, - data.items()) - - OrderedDumper.add_multi_representer(OrderedDict, _dict_representer) - return yaml.dump(data, stream, OrderedDumper, **kwargs) - - return ordered_dump(self, Dumper=yaml.SafeDumper, - default_flow_style=False).strip() - - -class HomeAssistantAPI(object): - """Class to access Home Assistant REST API.""" - - def __init__(self, api_url, password=None): - """Initialize the class object.""" - self.cache = {} - self.api_url = api_url - - if password is None: - password = self.auth() - self.password = password - - def auth(self): - """Prompt user to enter a password.""" - try: - return getpass("Enter password: ") - except KeyboardInterrupt: - print() - sys.exit(130) - - def get(self, endpoint='/', refresh=False): - """Wrapper to send a GET request to Home Assistant API.""" - if endpoint in self.cache and not refresh: - return self.cache[endpoint] - - url = self.api_url + endpoint - headers = {'x-ha-access': self.password or '', - 'content-type': 'application/json'} - - request = requests.get(url, headers=headers) - - if request.status_code == requests.codes.unauthorized: - self.password = self.auth() - return self.get(endpoint=endpoint, refresh=refresh) - else: - request.raise_for_status() - - self.cache[endpoint] = request - return request - - def get_config(self, **kwargs): - """Get config from Home Assistant REST API.""" - request = self.get('/config', **kwargs) - return request.json() - - def get_states(self, **kwargs): - """Get states from Home Assistant REST API.""" - request = self.get('/states', **kwargs) - return request.json() - - -def backup_file(filepath, dry_run=False): - """Automatically create a rotating backup of a file.""" - # Return None if original file does not exist - if not os.path.exists(filepath): - return None - - # Find next backup file - c = 0 - while True: - backupfile = "{}.{}".format(filepath, c) - if not os.path.exists(backupfile): - break - c += 1 - - # Only move the file if this is not a dry run - if not dry_run: - shutil.move(filepath, backupfile) - - # Return the backup filename - return backupfile - - -def main(): - """Main program function.""" - global args - - if args.debug: - log_level = logging.DEBUG - else: - log_level = logging.INFO - logging.basicConfig(level=log_level) - - try: - from colorlog import ColoredFormatter - logging.getLogger().handlers[0].setFormatter(ColoredFormatter( - "%(log_color)s[%(levelname)s] %(message)s%(reset)s", - datefmt="", - reset=True, - log_colors={ - 'DEBUG': 'cyan', - 'INFO': 'green', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', - } - )) - except ImportError: - pass - - # Detect input source (file, API URL, or - [stdin]) - if args.input == '-': - # Input is stdin - _LOGGER.debug("Reading input from stdin") - if not sys.stdin.isatty(): - states_json = json.load(sys.stdin) - else: - _LOGGER.error("Cannot read input from stdin") - return 1 - elif (args.input.lower().startswith('http://') or - args.input.lower().startswith('https://')): - # Input is API URL - _LOGGER.debug("Reading input from URL: {}".format(args.input)) - hass = HomeAssistantAPI(args.input, args.password) - try: - states_json = hass.get_states() - except requests.exceptions.ConnectionError: - _LOGGER.error("Could not connect to API URL: " - "{}".format(args.input)) - return 1 - else: - # Input is file - _LOGGER.debug("Reading input from file: {}".format(args.input)) - try: - with open(args.input, 'r') as f: - states_json = json.load(f) - except FileNotFoundError: - _LOGGER.error("{}: No such file".format(args.input)) - return 1 - except PermissionError: - _LOGGER.error("{}: Permission denied".format(args.input)) - return 1 - - # Convert to Lovelace UI - lovelace = Lovelace(states_json, title=args.title) - - # Get YAML dump of Lovelace UI - dump = lovelace.dump() - - # Set our output file - outfile = args.output - - # Try to create a backup - try: - backupfile = backup_file(outfile, dry_run=args.dry_run) - if backupfile: - _LOGGER.error("{}: file exists, backed up to: {}" - "".format(outfile, backupfile)) - except PermissionError: - _LOGGER.error("Could not create backup: {}: Permission denied" - "".format(outfile)) - return 1 - - if not args.dry_run: - # Try to output to file - try: - with open(outfile, 'w') as f: - f.write(""" -# This file was automatically generated by lovelace_migrate.py -# https://github.com/dale3h/python-lovelace - -""") - f.write(dump) - - _LOGGER.info("Lovelace UI successfully written to: {}" - "".format(outfile)) - except PermissionError: - _LOGGER.error("Could not write to file: {}: Permission denied" - "".format(outfile)) - return 1 - - else: - # Output Lovelace YAML to stdout - print(lovelace.dump()) - - # Return with a normal exit code - return 0 - - -if __name__ == '__main__': - sys.exit(main())