fixes
This commit is contained in:
Binary file not shown.
@@ -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='<api-url|file>', nargs='?',
|
||||
help="Home Assistant REST API URL or states JSON file")
|
||||
|
||||
# Optional arguments
|
||||
parser.add_argument(
|
||||
'-o', '--output', metavar='<file>', default='ui-lovelace.yaml',
|
||||
help="write output to <file> (default: ui-lovelace.yaml)")
|
||||
parser.add_argument(
|
||||
'-p', '--password', metavar='<password>', nargs='?',
|
||||
default=False, const=None,
|
||||
help="Home Assistant API password")
|
||||
parser.add_argument(
|
||||
'-t', '--title', metavar='<title>', 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())
|
||||
Reference in New Issue
Block a user