Compare commits

...

18 Commits

Author SHA1 Message Date
andvikt
f5519a595d Bump version: 1.1.0b0 → 1.1.0 2021-10-29 21:05:14 +03:00
andvikt
ee26759003 Bump version: 1.0.10b20 → 1.1.0b0 2021-10-29 21:04:30 +03:00
andvikt
8869ad9cee new config adressation 2021-10-29 20:14:18 +03:00
andvikt
27f1e05a3a new config adressation 2021-10-29 20:13:31 +03:00
andvikt
7fdc2fc9ff new config adressation 2021-10-29 20:12:59 +03:00
andvikt
28bf4393f0 Bump version: 1.0.10b19 → 1.0.10b20 2021-10-29 19:53:01 +03:00
andvikt
5ecb246eff fix brightness 2021-10-29 19:52:10 +03:00
Викторов Андрей Германович
3ee464a896 Bump version: 1.0.10b18 → 1.0.10b19 2021-10-29 15:26:28 +03:00
Викторов Андрей Германович
6368fd7cfc add fill_na 2021-10-29 15:26:21 +03:00
Викторов Андрей Германович
647baa9bb1 Bump version: 1.0.10b17 → 1.0.10b18 2021-10-29 14:56:34 +03:00
Викторов Андрей Германович
614525b315 fix customization i2c 2021-10-29 14:56:30 +03:00
Викторов Андрей Германович
a80b9b4293 Bump version: 1.0.10b16 → 1.0.10b17 2021-10-29 13:22:20 +03:00
Викторов Андрей Германович
f6ae5a9f04 add fill_na
add range
2021-10-29 13:22:14 +03:00
Викторов Андрей Германович
9eed9995fb Bump version: 1.0.10b15 → 1.0.10b16 2021-10-29 12:53:23 +03:00
Викторов Андрей Германович
40ff7cca2e add brightness range 2021-10-29 12:53:17 +03:00
Викторов Андрей Германович
86f58bbfd9 Bump version: 1.0.10b14 → 1.0.10b15 2021-10-29 12:18:29 +03:00
Викторов Андрей Германович
633d5e67bd Bump version: 1.0.10b13 → 1.0.10b14 2021-10-29 12:15:23 +03:00
Викторов Андрей Германович
79e3f33345 add sensor filters 2021-10-29 12:15:02 +03:00
10 changed files with 125 additions and 42 deletions

View File

@@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 1.0.10b13 current_version = 1.1.0
parse = (?P<major>\d+)(\.(?P<minor>\d+))(\.(?P<patch>\d+))(?P<release>[bf]*)(?P<build>\d*) parse = (?P<major>\d+)(\.(?P<minor>\d+))(\.(?P<patch>\d+))(?P<release>[bf]*)(?P<build>\d*)
commit = True commit = True
tag = True tag = True

View File

@@ -17,7 +17,7 @@ from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \ CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \
RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE, \ RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE, \
CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH, CONF_FILL_NA
from .hub import MegaD from .hub import MegaD
from .config_flow import ConfigFlow from .config_flow import ConfigFlow
from .http import MegaView from .http import MegaView
@@ -52,6 +52,14 @@ LED_LIGHT = \
CUSTOMIZE_PORT = { CUSTOMIZE_PORT = {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
vol.Optional(CONF_FILL_NA, default='last'): vol.Any(
'last',
'none'
),
vol.Optional(CONF_RANGE, description='диапазон диммирования'): [
vol.Range(0, 255),
vol.Range(0, 255),
],
vol.Optional(CONF_INVERT, default=False): bool, vol.Optional(CONF_INVERT, default=False): bool,
vol.Optional(CONF_NAME): vol.Any(str, { vol.Optional(CONF_NAME): vol.Any(str, {
vol.Required(str): str vol.Required(str): str

View File

@@ -45,6 +45,7 @@ CONF_SMOOTH = 'smooth'
CONF_WHITE_SEP = 'white_sep' CONF_WHITE_SEP = 'white_sep'
CONF_CHIP = 'chip' CONF_CHIP = 'chip'
CONF_RANGE = 'range' CONF_RANGE = 'range'
CONF_FILL_NA = 'fill_na'
CONF_FILTER_VALUES = 'filter_values' CONF_FILTER_VALUES = 'filter_values'
CONF_FILTER_SCALE = 'filter_scale' CONF_FILTER_SCALE = 'filter_scale'
CONF_FILTER_LOW = 'filter_low' CONF_FILTER_LOW = 'filter_low'

View File

@@ -12,7 +12,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.restore_state import RestoreEntity
from . import hub as h from . import hub as h
from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \ from .const import DOMAIN, CONF_CUSTOM, CONF_INVERT, EVENT_BINARY_SENSOR, LONG, \
LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY, CONF_SMOOTH LONG_RELEASE, RELEASE, PRESS, SINGLE_CLICK, DOUBLE_CLICK, EVENT_BINARY, CONF_SMOOTH, CONF_RANGE
_events_on = False _events_on = False
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -54,7 +54,6 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
smooth=None, smooth=None,
**kwargs, **kwargs,
): ):
super().__init__(mega.updater)
self._smooth = smooth self._smooth = smooth
self.http_cmd = http_cmd self.http_cmd = http_cmd
self._state: State = None self._state: State = None
@@ -85,6 +84,8 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self._can_smooth_hard = None self._can_smooth_hard = None
if self.http_cmd == 'ds2413': if self.http_cmd == 'ds2413':
self.mega.ds2413_ports |= {self.port} self.mega.ds2413_ports |= {self.port}
super().__init__(coordinator=mega.updater)
@property @property
def is_ws(self): def is_ws(self):
@@ -126,10 +127,13 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
def customize(self): def customize(self):
if self._customize is not None: if self._customize is not None:
return self._customize return self._customize
if self.hass is None: if self.hass is None or self.entity_id is None:
return {} return {}
if self._customize is None: if self._customize is None:
c_entity_id = self.hass.data.get(DOMAIN, {}).get('entities', {}).get(self.entity_id, {}) c_entity_id = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM).get('entities', {}).get(self.entity_id, {})
self.lg.debug(
'customize %s with %s', self.entity_id, c_entity_id
)
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {} c = c.get(self._mega_id) or {}
c = c.get(self.port) or {} c = c.get(self.port) or {}
@@ -168,9 +172,9 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
@property @property
def lg(self) -> logging.Logger: def lg(self) -> logging.Logger:
if self._lg is None: # if self._lg is None:
self._lg = self.mega.lg.getChild(self._name or self.unique_id) # self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return self._lg return _LOGGER
@property @property
def available(self) -> bool: def available(self) -> bool:
@@ -303,35 +307,43 @@ class MegaOutPort(MegaPushEntity):
else: else:
return 255 return 255
@property
def range(self) -> typing.List[int]:
return self.customize.get(CONF_RANGE, [0, 255])
@property @property
def invert(self): def invert(self):
return self.customize.get(CONF_INVERT, False) return self.customize.get(CONF_INVERT, False)
@property @property
def brightness(self): def brightness(self):
ret = None
if not self.dimmer: if not self.dimmer:
return return
val = self.mega.values.get(self.port, {}) val = self.mega.values.get(self.port, {})
if isinstance(val, dict) and len(val) == 0 and self._state is not None: if isinstance(val, dict) and len(val) == 0 and self._state is not None:
return self._state.attributes.get("brightness") ret = safe_int(self._state.attributes.get("brightness"), def_on=self.max_dim, def_off=0, def_val=0)
ret = self._calc_brightness(ret)
elif isinstance(self.port, str) and 'e' in self.port: elif isinstance(self.port, str) and 'e' in self.port:
if isinstance(val, str): if isinstance(val, str):
val = safe_int(val) val = safe_int(val, def_on=self.max_dim, def_off=0, def_val=0)
else: else:
val = 0 val = 0
if val == 0: if val == 0:
return self._brightness ret = self._brightness
elif isinstance(val, (int, float)): elif isinstance(val, (int, float)):
return int(val / self.dimmer_scale) ret = int(val / self.dimmer_scale)
elif val is not None: elif val is not None:
val = val.get("value") val = val.get("value")
if val is None: if val is None:
return return
try: try:
val = int(val) val = int(val)
return val ret = val
except Exception: except Exception:
pass pass
ret = self._cal_reverse_brightness(ret)
return ret
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
@@ -417,6 +429,28 @@ class MegaOutPort(MegaPushEntity):
updater=partial(self.update_from_smooth, update_state=update_state), updater=partial(self.update_from_smooth, update_state=update_state),
)) ))
def _calc_brightness(self, brightness):
if brightness is None:
brightness = 0
pct = brightness / 255
pct = max((0, pct))
pct = min((pct, 1))
l, h = self.range
d = h - l
brightness = round(pct * d + l)
return brightness
def _cal_reverse_brightness(self, brightness):
if brightness is None:
brightness = 0
l, h = self.range
d = h - l
pct = (brightness - l) / d
pct = max((0, pct))
pct = min((pct, 1))
brightness = round(pct * 255)
return brightness
async def async_turn_on(self, brightness=None, transition=None, **kwargs): async def async_turn_on(self, brightness=None, transition=None, **kwargs):
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
return return
@@ -426,6 +460,7 @@ class MegaOutPort(MegaPushEntity):
if not self.is_on and (brightness is None or brightness == 0): if not self.is_on and (brightness is None or brightness == 0):
brightness = self._restore_brightness brightness = self._restore_brightness
brightness = brightness or self.brightness or 255 brightness = brightness or self.brightness or 255
brightness = self._calc_brightness(brightness)
_prev = safe_int(self.brightness) or 0 _prev = safe_int(self.brightness) or 0
self._brightness = brightness self._brightness = brightness
if self.dimmer and brightness == 0: if self.dimmer and brightness == 0:
@@ -469,7 +504,7 @@ class MegaOutPort(MegaPushEntity):
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
return return
self._last_called = time.time() self._last_called = time.time()
self._restore_brightness = safe_int(self._brightness) self._restore_brightness = self._cal_reverse_brightness(safe_int(self._brightness))
if not self.dimmer: if not self.dimmer:
transition = None transition = None
cmd = "0" if not self.invert else "1" cmd = "0" if not self.invert else "1"
@@ -505,12 +540,12 @@ class MegaOutPort(MegaPushEntity):
def safe_int(v): def safe_int(v, def_on=1, def_off=0, def_val=None):
if v == 'ON': if v == 'ON':
return 1 return def_on
elif v == 'OFF': elif v == 'OFF':
return 0 return def_off
try: try:
return int(v) return int(v)
except (ValueError, TypeError): except (ValueError, TypeError):
return None return def_val

View File

@@ -427,7 +427,7 @@ class MegaD:
def subscribe(self, port, callback): def subscribe(self, port, callback):
port = int_ignore(port) port = int_ignore(port)
self.lg.debug( self.lg.debug(
f'subscribe %s %s', port, callback f'subscribe %s %s', port, str(callback)
) )
self.http.callbacks[self.id][port].append(callback) self.http.callbacks[self.id][port].append(callback)

View File

@@ -1,6 +1,7 @@
"""Platform for light integration.""" """Platform for light integration."""
import asyncio import asyncio
import logging import logging
import typing
from datetime import timedelta, datetime from datetime import timedelta, datetime
from functools import partial from functools import partial

View File

@@ -15,5 +15,5 @@
"@andvikt" "@andvikt"
], ],
"issue_tracker": "https://github.com/andvikt/mega_hacs/issues", "issue_tracker": "https://github.com/andvikt/mega_hacs/issues",
"version": "v1.0.10b13" "version": "v1.1.0"
} }

View File

@@ -21,7 +21,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .entities import MegaPushEntity from .entities import MegaPushEntity
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT, DOMAIN, CONF_CUSTOM, \ from .const import CONF_KEY, TEMP, HUM, W1, W1BUS, CONF_CONV_TEMPLATE, CONF_HEX_TO_FLOAT, DOMAIN, CONF_CUSTOM, \
CONF_SKIP, CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH CONF_SKIP, CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH, CONF_FILL_NA
from .hub import MegaD from .hub import MegaD
import re import re
@@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
hub.skip_ports |= {port} hub.skip_ports |= {port}
continue continue
for data in cfg: for data in cfg:
hub.lg.debug(f'add sensor on port %s with data %s', port, data) hub.lg.debug(f'add sensor on port %s with data %s, constructor: %s', port, data, _constructors[tp])
sensor = _constructors[tp]( sensor = _constructors[tp](
mega=hub, mega=hub,
port=port, port=port,
@@ -111,25 +111,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
class FilterBadValues(MegaPushEntity): class FilterBadValues(MegaPushEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._prev_value = None self._prev_value = None
super().__init__(*args, **kwargs)
def filter_value(self, value): def filter_value(self, value):
self.lg.debug( if value \
'value=%s filter_low=%s filter_high=%s filter_scale=%s prev_value=%s filter_values=%s', in self.filter_values \
(value, self.filter_low, self.filter_high, self.filter_scale, self._prev_value, self.filter_values)
)
if value in self.filter_values \
or (self.filter_low is not None and value < self.filter_low) \ or (self.filter_low is not None and value < self.filter_low) \
or (self.filter_high is not None and value > self.filter_high) \ or (self.filter_high is not None and value > self.filter_high) \
or ( or (
self._prev_value is not None self._prev_value is not None
and self.filter_scale is not None and self.filter_scale is not None
and ( and (
abs(value - self._prev_value) / self._prev_value > self.filter_scale abs(value - self._prev_value) / self._prev_value > self.filter_scale
) )
): ):
value = self._prev_value if self.fill_na == 'last':
value = self._prev_value
else:
value = None
self._prev_value = value self._prev_value = value
return value return value
@@ -149,6 +149,10 @@ class FilterBadValues(MegaPushEntity):
def filter_high(self): def filter_high(self):
return self.customize.get(CONF_FILTER_HIGH, self.mega.customize.get(CONF_FILTER_HIGH, None)) return self.customize.get(CONF_FILTER_HIGH, self.mega.customize.get(CONF_FILTER_HIGH, None))
@property
def fill_na(self):
return self.customize.get(CONF_FILL_NA, 'last')
class MegaI2C(FilterBadValues): class MegaI2C(FilterBadValues):
@@ -167,7 +171,12 @@ class MegaI2C(FilterBadValues):
@property @property
def customize(self): def customize(self):
return super().customize.get(self.id_suffix, {}) or {} ret = super().customize
_old = ret.get(self.id_suffix)
if _old is not None:
ret = ret.copy()
ret.update(_old)
return ret
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
@@ -187,7 +196,6 @@ class MegaI2C(FilterBadValues):
@property @property
def state(self): def state(self):
# self.lg.debug(f'get % all states: %', self._params, self.mega.values)
ret = self.mega.values.get(self._params) ret = self.mega.values.get(self._params)
if self.customize.get(CONF_HEX_TO_FLOAT): if self.customize.get(CONF_HEX_TO_FLOAT):
try: try:
@@ -203,7 +211,8 @@ class MegaI2C(FilterBadValues):
except: except:
ret = ret ret = ret
ret = self.filter_value(ret) ret = self.filter_value(ret)
return str(ret) if ret is not None:
return str(ret)
@property @property
def device_class(self): def device_class(self):
@@ -227,8 +236,8 @@ class Mega1WSensor(FilterBadValues):
:param patt: pattern to extract value, must have at least one group that will contain parsed value :param patt: pattern to extract value, must have at least one group that will contain parsed value
""" """
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._value = None
self.key = key self.key = key
self._value = None
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self.mega.sensors.append(self) self.mega.sensors.append(self)
@@ -251,7 +260,7 @@ class Mega1WSensor(FilterBadValues):
if self.key: if self.key:
return super().unique_id + f'_{self.key}' return super().unique_id + f'_{self.key}'
else: else:
return super(Mega1WSensor, self).unique_id return super().unique_id
@property @property
def device_class(self): def device_class(self):
@@ -268,6 +277,8 @@ class Mega1WSensor(FilterBadValues):
@property @property
def state(self): def state(self):
ret = None ret = None
if not hasattr(self, 'key'):
return None
if self.key: if self.key:
try: try:
ret = self.mega.values.get(self.port, {}) ret = self.mega.values.get(self.port, {})
@@ -280,13 +291,15 @@ class Mega1WSensor(FilterBadValues):
return return
else: else:
ret = self.mega.values.get(self.port, {}).get('value') ret = self.mega.values.get(self.port, {}).get('value')
if ret is None and self._state is not None: if ret is None and self.fill_na == 'fill_na' and self.prev_value is not None:
ret = self.prev_value
elif ret is None and self.fill_na == 'fill_na' and self._state is not None:
ret = self._state.state ret = self._state.state
try: try:
ret = float(ret) ret = float(ret)
ret = str(ret) ret = str(ret)
except: except:
self.lg.warning(f'could not convert to float "{ret}"') self.lg.debug(f'could not convert to float "{ret}"')
ret = self.prev_value ret = self.prev_value
if self.customize.get(CONF_HEX_TO_FLOAT): if self.customize.get(CONF_HEX_TO_FLOAT):
try: try:
@@ -303,14 +316,18 @@ class Mega1WSensor(FilterBadValues):
pass pass
ret = self.filter_value(ret) ret = self.filter_value(ret)
self.prev_value = ret self.prev_value = ret
return str(ret) if ret is not None:
return str(ret)
@property @property
def name(self): def name(self):
n = super().name n = super().name
c = self.customize.get(CONF_NAME, {}) c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict): if isinstance(c, dict):
c = c.get(self.key) try:
c = c.get(self.key)
except:
pass
return c or n return c or n

View File

@@ -1,6 +1,25 @@
С помощью yaml-конфигурации можно кастомизировать ваши устройства. С помощью yaml-конфигурации можно кастомизировать ваши устройства.
## Основное ## Основное
!!! note "Альтернативная адресация"
Начиная с v1.1.0 большинство параметров объектов можно записывать в более простой и понятной форме:
```yaml
mega: # название интеграции
entities:
sensor.some_sensor: #entity_id как в интерфейсе HA
filter_low: 20
filter_high: 40
```
Рекомендуется пользоваться именно этим способом, тк он более логичный и простой.
Некоторые параметры по своей логике (влияют на entity_id) не могут быть записаны таким образом, среди них:
- domain
- skip
- name
Остальные параметры можно записывать используя новый entities
Конфиг записывается стандартным образом в файл `configuration.yaml`, начинаем с Конфиг записывается стандартным образом в файл `configuration.yaml`, начинаем с
указания названия интеграции: указания названия интеграции:
```yaml hl_lines="1" ```yaml hl_lines="1"
@@ -62,7 +81,7 @@ mega:
!!! note "" !!! note ""
- **smooth** (float, 0): программное плавное диммирование. Это поле отвечает за кол-во секунд, за которое яркость - **smooth** (float, 0): программное плавное диммирование. Это поле отвечает за кол-во секунд, за которое яркость
диммера набирает от 0 до 100% диммера набирает от 0 до 100%
- **limits** (list[int, int], [0, 255]), *начиная с версии 1.1.0*: границы диммирования в абсолютных единицах контроллера. При диммировании 1% - **range** (list[int, int], [0, 255]), *начиная с версии 1.1.0*: границы диммирования в абсолютных единицах 0..255. При диммировании 1%
будет равен левой границе, 100% - правой. будет равен левой границе, 100% - правой.
[Подробнее про плавное диммирование](smooth.md) [Подробнее про плавное диммирование](smooth.md)
### MegaD-16R-XT, MegaD-16PWM ### MegaD-16R-XT, MegaD-16PWM
@@ -165,6 +184,8 @@ some_led2:
- **filter_scale** (float, none): значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если - **filter_scale** (float, none): значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если
установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться. установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться.
[Доступно так же глобальное значение](#filter_scale) [Доступно так же глобальное значение](#filter_scale)
- **fill_na** (str, last): чем заполнять пропуски, по-умолчанию last, что означает последнее значение, можно так же поставить none-тогда будут пропуски (разрывы на графике).
При этом есть так же особенности адресации, так для сенсора на одном порте с одним значением: При этом есть так же особенности адресации, так для сенсора на одном порте с одним значением:
```yaml ```yaml
36: 36:

View File

@@ -22,7 +22,7 @@ nav:
- Конфигурация: - Конфигурация:
- В интерфейсе: settings.md - В интерфейсе: settings.md
- Настройка обратной связи: http.md - Настройка обратной связи: http.md
- YAML-конфиг: yaml.md - Кастомизация: yaml.md
- i2c: i2c.md - i2c: i2c.md
- Плавные переходы: smooth.md - Плавные переходы: smooth.md
- Автоматизация: - Автоматизация: