From c1db46839e88e7a507abf86d02a5bf93c64d033b Mon Sep 17 00:00:00 2001 From: monster Date: Sat, 9 Feb 2019 19:15:09 +0300 Subject: [PATCH] added lovelace --- hass/settings/configuration.yaml | 6 +- hass/settings/configuration/group/_views.yaml | 8 + .../group/stats/power_stats.yaml | 6 + .../group/stats/water_stats.yaml | 9 + .../configuration/utility_meter/power.yaml | 9 + .../utility_meter/water_cold.yaml | 9 + .../utility_meter/water_hot.yaml | 9 + hass/settings/lovelace_migrate.py | 844 ++++++++++++++++++ hass/settings/ui-lovelace.yaml | 245 +++++ .../settings/configuration.yaml.sample | 4 +- 10 files changed, 1146 insertions(+), 3 deletions(-) create mode 100644 hass/settings/configuration/group/stats/power_stats.yaml create mode 100644 hass/settings/configuration/group/stats/water_stats.yaml create mode 100644 hass/settings/configuration/utility_meter/power.yaml create mode 100644 hass/settings/configuration/utility_meter/water_cold.yaml create mode 100644 hass/settings/configuration/utility_meter/water_hot.yaml create mode 100644 hass/settings/lovelace_migrate.py create mode 100644 hass/settings/ui-lovelace.yaml diff --git a/hass/settings/configuration.yaml b/hass/settings/configuration.yaml index ddfd69b..c117bb2 100644 --- a/hass/settings/configuration.yaml +++ b/hass/settings/configuration.yaml @@ -26,7 +26,8 @@ lovelace: # Enables configuration UI config: discovery: - +system_health: + # Enables support for tracking state changes over time recorder: !include configuration/recorder.yaml history: @@ -63,4 +64,5 @@ homekit: !include configuration/homekit/homekit.yaml #power meter modbus: !include configuration/modbus.yaml -mysensors: !include configuration/mysensors.yaml \ No newline at end of file +mysensors: !include configuration/mysensors.yaml +utility_meter: !include_dir_merge_list configuration/utility_meter \ No newline at end of file diff --git a/hass/settings/configuration/group/_views.yaml b/hass/settings/configuration/group/_views.yaml index 82bbd8f..6ee6e15 100644 --- a/hass/settings/configuration/group/_views.yaml +++ b/hass/settings/configuration/group/_views.yaml @@ -40,6 +40,14 @@ alarmclock: - group.alarmclock_weekdays - group.alarmclock_weekend +stats: + view: yes + name: Статистика + icon: mdi:chart-areaspline + entities: + - group.power_stats + - group.water_stats + settings: view: yes name: Настройки diff --git a/hass/settings/configuration/group/stats/power_stats.yaml b/hass/settings/configuration/group/stats/power_stats.yaml new file mode 100644 index 0000000..9d329fa --- /dev/null +++ b/hass/settings/configuration/group/stats/power_stats.yaml @@ -0,0 +1,6 @@ +power_stats: + name: Статистика электропитания + entities: + - utility_meter.power_dialy + - utility_meter.power_monthly + - utility_meter.power_hourly \ No newline at end of file diff --git a/hass/settings/configuration/group/stats/water_stats.yaml b/hass/settings/configuration/group/stats/water_stats.yaml new file mode 100644 index 0000000..f9b8519 --- /dev/null +++ b/hass/settings/configuration/group/stats/water_stats.yaml @@ -0,0 +1,9 @@ +water_stats: + name: Статистика потребления воды + entities: + - utility_meter.counter_cold_hourly + - utility_meter.counter_cold_dialy + - utility_meter.counter_cold_monthly + - utility_meter.counter_hot_hourly + - utility_meter.counter_hot_dialy + - utility_meter.counter_hot_monthly diff --git a/hass/settings/configuration/utility_meter/power.yaml b/hass/settings/configuration/utility_meter/power.yaml new file mode 100644 index 0000000..6dd52b9 --- /dev/null +++ b/hass/settings/configuration/utility_meter/power.yaml @@ -0,0 +1,9 @@ +power_hourly: + source: sensor.power_total + cycle: hourly +power_dialy: + source: sensor.power_total + cycle: daily +power_monthly: + source: sensor.power_total + cycle: monthly diff --git a/hass/settings/configuration/utility_meter/water_cold.yaml b/hass/settings/configuration/utility_meter/water_cold.yaml new file mode 100644 index 0000000..e93ae0c --- /dev/null +++ b/hass/settings/configuration/utility_meter/water_cold.yaml @@ -0,0 +1,9 @@ +counter_cold_hourly: + source: sensor.counter_cold + cycle: hourly +counter_cold_dialy: + source: sensor.counter_cold + cycle: daily +counter_cold_monthly: + source: sensor.counter_cold + cycle: monthly diff --git a/hass/settings/configuration/utility_meter/water_hot.yaml b/hass/settings/configuration/utility_meter/water_hot.yaml new file mode 100644 index 0000000..b73ab8f --- /dev/null +++ b/hass/settings/configuration/utility_meter/water_hot.yaml @@ -0,0 +1,9 @@ +counter_hot_hourly: + source: sensor.counter_hot + cycle: hourly +counter_hot_dialy: + source: sensor.counter_hot + cycle: daily +counter_hot_monthly: + source: sensor.counter_hot + cycle: monthly diff --git a/hass/settings/lovelace_migrate.py b/hass/settings/lovelace_migrate.py new file mode 100644 index 0000000..003a469 --- /dev/null +++ b/hass/settings/lovelace_migrate.py @@ -0,0 +1,844 @@ +""" +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()) diff --git a/hass/settings/ui-lovelace.yaml b/hass/settings/ui-lovelace.yaml new file mode 100644 index 0000000..a0a016b --- /dev/null +++ b/hass/settings/ui-lovelace.yaml @@ -0,0 +1,245 @@ + +# This file was automatically generated by lovelace_migrate.py +# https://github.com/dale3h/python-lovelace + +title: Home +views: + - title: "Статус" + icon: mdi:home + cards: + - type: entities + title: "Дом" + show_header_toggle: false + entities: + - alarm_control_panel.ha_alarm + - switch.water_valve + - vacuum.xiaomi_vacuum_cleaner + - switch.domofon_open + - switch.wifi_socket + - type: entities + title: "Гостиная" + show_header_toggle: false + entities: + - sensor.temperature_158d0001149b3c + - sensor.humidity_158d0001149b3c + - light.yeelight_rgb_34ce008fe328 + - light.yeelight_rgb_286c071065c7 + - light.yeelight_white_34ce0087ff33 + - light.yeelight_white_f0b4290e5444 + - light.yeelight_white_f0b429a8f6df + - cover.curtain_158d0002b0c46a + - switch.plug_158d0001104a0c + - switch.plug_158d00010dd98d + - climate.living_room + - type: entities + title: "Кухня" + show_header_toggle: false + entities: + - light.kitchen + - switch.kitchen_mini_light + - switch.plug_158d0001a68b18 + - switch.plug_158d0001a68c64 + - switch.plug_158d0001a4cfb4 + - switch.xiaomi_miio_switch + - type: entities + title: "Ванная" + show_header_toggle: false + entities: + - sensor.temperature_158d00012d9ad7 + - sensor.humidity_158d00012d9ad7 + - light.bath + - light.philips_led + - sensor.airfresher_remain + - type: entities + title: "Прихожая" + show_header_toggle: false + entities: + - light.hall + - type: entities + title: "Балкон" + show_header_toggle: false + entities: + - light.yeelight_white_286c07f12bed + - light.yeelight_white_f0b429a8d936 + - sensor.temperature_158d000117d37a + - sensor.humidity_158d000117d37a + - type: entities + title: "Улица" + show_header_toggle: false + entities: + - sensor.temperature_158d000149c128 + - sensor.humidity_158d000149c128 + - title: "Датчики" + icon: mdi:nfc + cards: + - type: entities + title: "Движение" + show_header_toggle: true + entities: + - binary_sensor.motion_sensor_158d0000e7c7ad + - binary_sensor.motion_sensor_158d0002044548 + - binary_sensor.motion_sensor_158d0000fbc664 + - binary_sensor.motion_sensor_158d0001535c97 + - binary_sensor.motion_sensor_158d0000f9546b + - binary_sensor.motion_sensor_158d000165fed5 + - binary_sensor.motion_sensor_158d0001537e21 + - binary_sensor.motion_sensor_158d0000f6b2fa + - binary_sensor.motion_sensor_158d0001e50ce4 + - binary_sensor.motion_sensor_158d0001e80f0d + - type: entities + title: "Протечка" + show_header_toggle: true + entities: + - binary_sensor.water_leak_sensor_158d0001d594fa + - binary_sensor.water_leak_sensor_158d0001c34960 + - binary_sensor.water_leak_sensor_158d0002150c07 + - binary_sensor.water_leak_sensor_158d0002112b98 + - binary_sensor.water_leak_sensor_158d0002113158 + - binary_sensor.water_leak_sensor_158d00023a48f0 + - type: entities + title: "Датчики дыма" + show_header_toggle: true + entities: + - binary_sensor.smoke_sensor_158d00023115ad + - type: entities + title: "Двери" + show_header_toggle: true + entities: + - binary_sensor.door_window_sensor_158d0001130141 + - binary_sensor.door_window_sensor_158d000111a469 + - binary_sensor.door_window_sensor_158d00010f7e19 + - binary_sensor.door_window_sensor_158d0002254373 + - binary_sensor.door_window_sensor_158d0001b77b1b + - binary_sensor.door_window_sensor_158d0001c0e46d + - binary_sensor.door_window_sensor_158d0001e74660 + - type: entities + title: "Счетчики вод\ + ы" + show_header_toggle: true + entities: + - sensor.epd_balance + - sensor.counter_cold + - sensor.counter_hot + - sensor.counter_mosru_cold + - sensor.counter_mosru_hot + - type: entities + title: "Метки" + show_header_toggle: true + entities: + - sensor.tag_mamik_band + - sensor.tag_monster_nut + - sensor.tag_nut_blue + - sensor.tag_nut_white + - sensor.tag_nut_guest + - type: entities + title: "Пристутстви\ + е" + show_header_toggle: true + entities: + - sensor.presence_hall + - sensor.presence_kitchen + - sensor.presence_balcony + - type: entities + title: "Электропита\ + ние" + show_header_toggle: true + entities: + - sensor.power_mosenergosbyt_balance + - sensor.power_main_total + - sensor.power_mosenergosbyt_total + - sensor.power_total + - sensor.power_voltage + - sensor.power_current + - sensor.power_frequency + - sensor.power_active_power + - sensor.power_reactive_power + - sensor.power_power_factor + - title: "Камеры" + icon: mdi:cctv + cards: + - type: picture-entity + title: enterance + entity: camera.enterance + camera_image: camera.enterance + show_info: true + tap_action: dialog + - title: "Будильники" + icon: mdi:alarm + cards: + - type: entities + title: "Будильник (бу\ + дни)" + show_header_toggle: false + entities: + - input_boolean.alarmclock_weekdays + - input_datetime.alarmclock_weekdays + - type: entities + title: "Будильник (вы\ + ходные)" + show_header_toggle: false + entities: + - input_boolean.alarmclock_weekend + - input_datetime.alarmclock_weekend + + - title: "Статистика" + icon: mdi:chart-areaspline + cards: [] + + - title: "Настройки" + icon: mdi:settings + cards: + - type: entities + title: "Контроль света" + show_header_toggle: true + entities: + - input_boolean.mainroom_light_control + - input_boolean.hall_light_control + - input_boolean.bath_light_control + - input_boolean.kitchen_light_control + - input_boolean.balcony_light_control + - type: entities + title: "Настройки автоматики" + show_header_toggle: true + entities: + - input_boolean.presence_control + - input_boolean.water_control + - input_boolean.water_leak_control + - input_boolean.mainroom_climate_control + - switch.domofon_mode + - type: entities + title: "Настройки отчетов" + show_header_toggle: true + entities: + - input_boolean.security_sensors_report + - input_boolean.battery_report + - input_boolean.water_counters_report + - input_boolean.work_presence_report + - input_boolean.lock_report + - type: entities + title: "Метки" + show_header_toggle: true + entities: + - input_boolean.tag_mamik_band + - input_boolean.tag_monster_nut + - input_boolean.tag_nut_blue + - input_boolean.tag_nut_white + - input_boolean.tag_nut_guest + - title: "Информация" + icon: mdi:monitor-multiple + cards: + - type: entities + title: "ОС" + show_header_toggle: true + entities: + - sensor.processor_use + - sensor.memory_free + - sensor.swap_use_percent + - sensor.disk_use_percent + - sensor.ssl_certificate_expiry + - type: entities + title: "Сеть" + show_header_toggle: true + entities: + - sensor.speedtest_ping + - sensor.speedtest_download + - sensor.speedtest_upload \ No newline at end of file diff --git a/zigbee2mqtt/settings/configuration.yaml.sample b/zigbee2mqtt/settings/configuration.yaml.sample index 6df5de7..3b2d755 100644 --- a/zigbee2mqtt/settings/configuration.yaml.sample +++ b/zigbee2mqtt/settings/configuration.yaml.sample @@ -10,7 +10,6 @@ serial: xxxxxxxxx disable_led: xxxxxxxxx advanced: xxxxxxxxx cache_state: xxxxxxxxx - # log_level: xxxxxxxxx devices: xxxxxxxxx '0x00158d0001e065e8': xxxxxxxxx friendly_name: xxxxxxxxx @@ -18,3 +17,6 @@ devices: xxxxxxxxx '0x00158d0002a55439': xxxxxxxxx friendly_name: xxxxxxxxx retain: xxxxxxxxx + '0x00158d0001e50ce4': xxxxxxxxx + friendly_name: xxxxxxxxx + retain: xxxxxxxxx