added lovelace
This commit is contained in:
@@ -26,6 +26,7 @@ lovelace:
|
|||||||
# Enables configuration UI
|
# Enables configuration UI
|
||||||
config:
|
config:
|
||||||
discovery:
|
discovery:
|
||||||
|
system_health:
|
||||||
|
|
||||||
# Enables support for tracking state changes over time
|
# Enables support for tracking state changes over time
|
||||||
recorder: !include configuration/recorder.yaml
|
recorder: !include configuration/recorder.yaml
|
||||||
@@ -64,3 +65,4 @@ homekit: !include configuration/homekit/homekit.yaml
|
|||||||
modbus: !include configuration/modbus.yaml
|
modbus: !include configuration/modbus.yaml
|
||||||
|
|
||||||
mysensors: !include configuration/mysensors.yaml
|
mysensors: !include configuration/mysensors.yaml
|
||||||
|
utility_meter: !include_dir_merge_list configuration/utility_meter
|
||||||
@@ -40,6 +40,14 @@ alarmclock:
|
|||||||
- group.alarmclock_weekdays
|
- group.alarmclock_weekdays
|
||||||
- group.alarmclock_weekend
|
- group.alarmclock_weekend
|
||||||
|
|
||||||
|
stats:
|
||||||
|
view: yes
|
||||||
|
name: Статистика
|
||||||
|
icon: mdi:chart-areaspline
|
||||||
|
entities:
|
||||||
|
- group.power_stats
|
||||||
|
- group.water_stats
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
view: yes
|
view: yes
|
||||||
name: Настройки
|
name: Настройки
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
power_stats:
|
||||||
|
name: Статистика электропитания
|
||||||
|
entities:
|
||||||
|
- utility_meter.power_dialy
|
||||||
|
- utility_meter.power_monthly
|
||||||
|
- utility_meter.power_hourly
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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='<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())
|
||||||
@@ -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
|
||||||
@@ -10,7 +10,6 @@ serial: xxxxxxxxx
|
|||||||
disable_led: xxxxxxxxx
|
disable_led: xxxxxxxxx
|
||||||
advanced: xxxxxxxxx
|
advanced: xxxxxxxxx
|
||||||
cache_state: xxxxxxxxx
|
cache_state: xxxxxxxxx
|
||||||
# log_level: xxxxxxxxx
|
|
||||||
devices: xxxxxxxxx
|
devices: xxxxxxxxx
|
||||||
'0x00158d0001e065e8': xxxxxxxxx
|
'0x00158d0001e065e8': xxxxxxxxx
|
||||||
friendly_name: xxxxxxxxx
|
friendly_name: xxxxxxxxx
|
||||||
@@ -18,3 +17,6 @@ devices: xxxxxxxxx
|
|||||||
'0x00158d0002a55439': xxxxxxxxx
|
'0x00158d0002a55439': xxxxxxxxx
|
||||||
friendly_name: xxxxxxxxx
|
friendly_name: xxxxxxxxx
|
||||||
retain: xxxxxxxxx
|
retain: xxxxxxxxx
|
||||||
|
'0x00158d0001e50ce4': xxxxxxxxx
|
||||||
|
friendly_name: xxxxxxxxx
|
||||||
|
retain: xxxxxxxxx
|
||||||
|
|||||||
Reference in New Issue
Block a user