mirror of
https://github.com/andvikt/mega_hacs.git
synced 2025-12-12 01:24:29 +05:00
Compare commits
156 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0659b84dc | ||
|
|
3cd38a57e0 | ||
|
|
261c628909 | ||
|
|
2af9cfbeaf | ||
|
|
60fad1f20f | ||
|
|
5c18e395da | ||
|
|
082c647110 | ||
|
|
21bc277b78 | ||
|
|
00e62ee83f | ||
|
|
b62406210c | ||
|
|
c45653b0ba | ||
|
|
683727b3ec | ||
|
|
2300d2750a | ||
|
|
ac8603f219 | ||
|
|
fbc1f31023 | ||
|
|
cd5ab3b689 | ||
|
|
0138f5e323 | ||
|
|
0ab4f6623c | ||
|
|
5664e1e929 | ||
|
|
bcf108fc9c | ||
|
|
ca3f90374b | ||
|
|
d15cce8061 | ||
|
|
4e83d81004 | ||
|
|
9ceb544c1a | ||
|
|
c8b3cb60de | ||
|
|
bd550442d4 | ||
|
|
8903628b36 | ||
|
|
e1a6637f28 | ||
|
|
a346cb3fd7 | ||
|
|
d6ef137e75 | ||
|
|
d7180c0477 | ||
|
|
1ceb7e4766 | ||
|
|
ef46ac2b2b | ||
|
|
e44aef35fb | ||
|
|
a860ba9822 | ||
|
|
2463b270e5 | ||
|
|
ce2969c1e7 | ||
|
|
34056273f3 | ||
|
|
19313d3b35 | ||
|
|
cb14f9aa2a | ||
|
|
b908068315 | ||
|
|
c11de5c2b9 | ||
|
|
679b53bbd3 | ||
|
|
2c2b3aab74 | ||
|
|
a77794dff7 | ||
|
|
ebb513b83d | ||
|
|
f5519a595d | ||
|
|
ee26759003 | ||
|
|
8869ad9cee | ||
|
|
27f1e05a3a | ||
|
|
7fdc2fc9ff | ||
|
|
28bf4393f0 | ||
|
|
5ecb246eff | ||
|
|
3ee464a896 | ||
|
|
6368fd7cfc | ||
|
|
647baa9bb1 | ||
|
|
614525b315 | ||
|
|
a80b9b4293 | ||
|
|
f6ae5a9f04 | ||
|
|
9eed9995fb | ||
|
|
40ff7cca2e | ||
|
|
86f58bbfd9 | ||
|
|
633d5e67bd | ||
|
|
79e3f33345 | ||
|
|
74235a39ad | ||
|
|
61bcbbd103 | ||
|
|
6411e6e5f9 | ||
|
|
2020a840a8 | ||
|
|
f6c2cd025e | ||
|
|
ab599cd59d | ||
|
|
a349cbd4a4 | ||
|
|
bd1ba0379f | ||
|
|
70016dd69d | ||
|
|
5617e7afa5 | ||
|
|
bcdec2d793 | ||
|
|
c4aae6b3ec | ||
|
|
58d243dcc3 | ||
|
|
3488d4593a | ||
|
|
76b8988b83 | ||
|
|
4145eebbdd | ||
|
|
d4dddc61fd | ||
|
|
bc34c1a0b3 | ||
|
|
2662b9c3bd | ||
|
|
8399bbd1de | ||
|
|
975b2e8a38 | ||
|
|
919f1ceb12 | ||
|
|
2c58f8e67e | ||
|
|
1c65c341b7 | ||
|
|
2ef0ebf702 | ||
|
|
b8ceec0386 | ||
|
|
496713cfec | ||
|
|
1512238763 | ||
|
|
ce1987d0c0 | ||
|
|
b4521a93bb | ||
|
|
9322f9cae1 | ||
|
|
52ace90600 | ||
|
|
8738a936fe | ||
|
|
2490e09748 | ||
|
|
e134a874fa | ||
|
|
6fc5391490 | ||
|
|
d37fa11ab3 | ||
|
|
1db5670cfd | ||
|
|
74b6e200a5 | ||
|
|
3737bf91a3 | ||
|
|
747b8e6c36 | ||
|
|
fec5cef05c | ||
|
|
1ec581b08b | ||
|
|
c62a5a2d92 | ||
|
|
b6349eece8 | ||
|
|
4300676c23 | ||
|
|
a78a87662b | ||
|
|
530daea77c | ||
|
|
0a72dd0f24 | ||
|
|
4ccc0618ce | ||
|
|
eadfc5dce0 | ||
|
|
eb7926057f | ||
|
|
c635cc9b36 | ||
|
|
bb73c35f2e | ||
|
|
11c8eba6cb | ||
|
|
c1029d146c | ||
|
|
693e2e9901 | ||
|
|
5795c943f5 | ||
|
|
641531f8dc | ||
|
|
7b953d06d5 | ||
|
|
912440c079 | ||
|
|
54f1ebeda3 | ||
|
|
87492be38f | ||
|
|
0fab2384b0 | ||
|
|
70a2e5bbb0 | ||
|
|
83c843d722 | ||
|
|
af684a6c00 | ||
|
|
d94efbe1a7 | ||
|
|
eaa46a99ae | ||
|
|
e65598fe63 | ||
|
|
e12ab45c04 | ||
|
|
7fb72be646 | ||
|
|
f9f97a91b6 | ||
|
|
ad1210d5cc | ||
|
|
687e80f729 | ||
|
|
7d777c9e82 | ||
|
|
c9f0e85f6a | ||
|
|
e75f8b91ef | ||
|
|
bf15d4f3f9 | ||
|
|
124ef36564 | ||
|
|
9e73191a91 | ||
|
|
1270ea2ee2 | ||
|
|
36433a7fdd | ||
|
|
5edf000ce8 | ||
|
|
b821d182b2 | ||
|
|
149d30e921 | ||
|
|
84f677656a | ||
|
|
a0900052dc | ||
|
|
b8d355f412 | ||
|
|
d3f76a88df | ||
|
|
fa2bb2674e | ||
|
|
a1ae4e294b |
21
.bumpversion.cfg
Normal file
21
.bumpversion.cfg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[bumpversion]
|
||||||
|
current_version = 1.1.8b4
|
||||||
|
parse = (?P<major>\d+)(\.(?P<minor>\d+))(\.(?P<patch>\d+))(?P<release>[bf]*)(?P<build>\d*)
|
||||||
|
commit = True
|
||||||
|
tag = True
|
||||||
|
serialize =
|
||||||
|
{major}.{minor}.{patch}{release}{build}
|
||||||
|
{major}.{minor}.{patch}
|
||||||
|
|
||||||
|
[bumpversion:part:release]
|
||||||
|
optional_value = f
|
||||||
|
values =
|
||||||
|
b
|
||||||
|
f
|
||||||
|
|
||||||
|
[bumpversion:part:build]
|
||||||
|
first_value = 0
|
||||||
|
|
||||||
|
[bumpversion:file:custom_components/mega/manifest.json]
|
||||||
|
search = "version": "v{current_version}"
|
||||||
|
replace = "version": "v{new_version}"
|
||||||
126
.experiment.py
126
.experiment.py
@@ -1,121 +1,15 @@
|
|||||||
from urllib.parse import urlparse, parse_qsl
|
order ='brg'
|
||||||
|
rgb = 'rgb'
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
map_to_order = [rgb.index(x) for x in order]
|
||||||
|
map_from_order = [order.index(x) for x in rgb]
|
||||||
page = '''
|
|
||||||
<html><head></head><body><a href="/sec/?pt=33">Back</a><br>0x15 - <a href="/sec/?pt=33&scl=32&i2c_dev=t67xx">T67XX</a><br>0x40 - <a href="/sec/?pt=33&scl=32&i2c_dev=htu21d">HTU21D</a>/PCA9685/HM3301<br>0x4a - <a href="/sec/?pt=33&scl=32&i2c_dev=max44009">MAX44009</a><br>
|
|
||||||
</body></html>
|
|
||||||
'''
|
|
||||||
|
|
||||||
from urllib.parse import parse_qsl, urlparse
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
from homeassistant.const import (
|
|
||||||
DEVICE_CLASS_HUMIDITY,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
DEVICE_CLASS_ILLUMINANCE,
|
|
||||||
DEVICE_CLASS_PRESSURE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_scan_page(page: str):
|
_rgb = [
|
||||||
ret = []
|
rgb[x] for x in map_to_order
|
||||||
req = []
|
]
|
||||||
page = BeautifulSoup(page, features="lxml")
|
_order = [
|
||||||
for x in page.find_all('a'):
|
_rgb[x] for x in map_from_order
|
||||||
params = x.get('href')
|
|
||||||
if params is None:
|
|
||||||
continue
|
|
||||||
params = dict(parse_qsl(urlparse(params).query))
|
|
||||||
if 'i2c_dev' in params:
|
|
||||||
dev = params['i2c_dev']
|
|
||||||
classes = i2c_classes.get(dev, [])
|
|
||||||
for i, c in enumerate(classes):
|
|
||||||
if c is Skip:
|
|
||||||
continue
|
|
||||||
elif c is Request:
|
|
||||||
req.append(params)
|
|
||||||
continue
|
|
||||||
elif isinstance(c, tuple):
|
|
||||||
suffix, c = c
|
|
||||||
elif isinstance(c, str):
|
|
||||||
suffix = c
|
|
||||||
else:
|
|
||||||
suffix = ''
|
|
||||||
if 'addr' in params:
|
|
||||||
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
|
|
||||||
if suffix:
|
|
||||||
_dev = f'{dev}_{suffix}'
|
|
||||||
else:
|
|
||||||
_dev = dev
|
|
||||||
params = params.copy()
|
|
||||||
if i > 0:
|
|
||||||
params['i2c_par'] = i
|
|
||||||
ret.append({
|
|
||||||
'id_suffix': _dev,
|
|
||||||
'device_class': c,
|
|
||||||
'params': params,
|
|
||||||
})
|
|
||||||
req.append(params)
|
|
||||||
return req, ret
|
|
||||||
|
|
||||||
|
|
||||||
class Skip:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
i2c_classes = {
|
|
||||||
'htu21d': [
|
|
||||||
DEVICE_CLASS_HUMIDITY,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
],
|
|
||||||
'sht31': [
|
|
||||||
DEVICE_CLASS_HUMIDITY,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
],
|
|
||||||
'max44009': [
|
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
|
||||||
],
|
|
||||||
'bh1750': [
|
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
|
||||||
],
|
|
||||||
'tsl2591': [
|
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
|
||||||
],
|
|
||||||
'bmp180': [
|
|
||||||
DEVICE_CLASS_PRESSURE,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
],
|
|
||||||
'bmx280': [
|
|
||||||
DEVICE_CLASS_PRESSURE,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
DEVICE_CLASS_HUMIDITY
|
|
||||||
],
|
|
||||||
'mlx90614': [
|
|
||||||
Skip,
|
|
||||||
('temp', DEVICE_CLASS_TEMPERATURE),
|
|
||||||
('object', DEVICE_CLASS_TEMPERATURE),
|
|
||||||
],
|
|
||||||
'ptsensor': [
|
|
||||||
Request, # запрос на измерение
|
|
||||||
DEVICE_CLASS_PRESSURE,
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
],
|
|
||||||
'mcp9600': [
|
|
||||||
DEVICE_CLASS_TEMPERATURE, # термопара
|
|
||||||
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
|
|
||||||
],
|
|
||||||
't67xx': [
|
|
||||||
None # для co2 нет класса в HA
|
|
||||||
],
|
|
||||||
'tmp117': [
|
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
]
|
]
|
||||||
}
|
|
||||||
|
|
||||||
print(parse_scan_page(page))
|
print(_rgb, _order)
|
||||||
6
.github/ISSUE_TEMPLATE/bug-report.md
vendored
6
.github/ISSUE_TEMPLATE/bug-report.md
vendored
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Bug report
|
name: Проблема
|
||||||
about: Create a report to help us improve
|
about: Обнаружен баг/проблема
|
||||||
title: ''
|
title: ''
|
||||||
labels: ''
|
labels: ''
|
||||||
assignees: ''
|
assignees: ''
|
||||||
@@ -24,4 +24,4 @@ megad firmware version:
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**LOG**
|
**LOG**
|
||||||
Просьба прикладывать детальный лог, подробная инструкция как включать отладку по [ссылке](https://github.com/andvikt/mega_hacs/wiki/Отладка)
|
Просьба прикладывать детальный лог, подробная инструкция как включать отладку по [ссылке](https://andvikt.github.io/mega_hacs/debug/)
|
||||||
|
|||||||
9
.github/ISSUE_TEMPLATE/enhance.md
vendored
Normal file
9
.github/ISSUE_TEMPLATE/enhance.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: Улучшение
|
||||||
|
about: Идея на улучшение интеграции
|
||||||
|
title: ''
|
||||||
|
labels: 'enhancement'
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
**Описание**
|
||||||
17
.github/workflows/main.yml
vendored
Normal file
17
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
name: Publish docs via GitHub Pages
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Deploy docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Deploy docs
|
||||||
|
uses: mhausenblas/mkdocs-deploy-gh-pages@master
|
||||||
|
# Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
*sync-conflict*
|
||||||
|
*.db
|
||||||
|
*.log*
|
||||||
|
*.yaml
|
||||||
|
**/__pycache__
|
||||||
|
test_*
|
||||||
|
temp_*
|
||||||
|
tr.py
|
||||||
|
experiment_*
|
||||||
|
example*
|
||||||
|
.HA_VERSION
|
||||||
|
.idea
|
||||||
|
.storage
|
||||||
|
site
|
||||||
|
.DS_Store
|
||||||
|
.venv
|
||||||
|
pyproject.toml
|
||||||
|
poetry.lock
|
||||||
@@ -6,83 +6,181 @@ from functools import partial
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_SCAN_INTERVAL, CONF_ID, CONF_NAME, CONF_DOMAIN,
|
CONF_NAME,
|
||||||
CONF_UNIT_OF_MEASUREMENT, CONF_HOST, CONF_VALUE_TEMPLATE, CONF_DEVICE_CLASS
|
CONF_DOMAIN,
|
||||||
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
CONF_VALUE_TEMPLATE,
|
||||||
|
CONF_DEVICE_CLASS,
|
||||||
|
CONF_PORT,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
from homeassistant.helpers.service import bind_hass
|
from homeassistant.helpers.service import bind_hass
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.components import mqtt
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
|
from .const import (
|
||||||
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
|
DOMAIN,
|
||||||
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN
|
CONF_INVERT,
|
||||||
|
PLATFORMS,
|
||||||
|
CONF_PORTS,
|
||||||
|
CONF_CUSTOM,
|
||||||
|
CONF_SKIP,
|
||||||
|
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,
|
||||||
|
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_FILL_NA,
|
||||||
|
CONF_MEGA_ID,
|
||||||
|
CONF_ADDR,
|
||||||
|
CONF_1WBUS,
|
||||||
|
)
|
||||||
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
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_port_n = vol.Any(int, str)
|
||||||
|
|
||||||
|
LED_LIGHT = {
|
||||||
|
str: vol.Any(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PORTS): vol.Any(
|
||||||
|
vol.ExactSequence([_port_n, _port_n, _port_n]),
|
||||||
|
vol.ExactSequence([_port_n, _port_n, _port_n, _port_n]),
|
||||||
|
msg="ports must be [R, G, B] or [R, G, B, W] of integers 0..255",
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_NAME): str,
|
||||||
|
vol.Optional(CONF_WHITE_SEP, default=True): bool,
|
||||||
|
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PORT): int,
|
||||||
|
vol.Required(CONF_WS28XX): True,
|
||||||
|
vol.Optional(CONF_CHIP, default=100): int,
|
||||||
|
vol.Optional(CONF_ORDER, default="rgb"): vol.Any(
|
||||||
|
*RGB_COMBINATIONS, msg=f"order must be one of {RGB_COMBINATIONS}"
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_SMOOTH, default=1): cv.time_period_seconds,
|
||||||
|
vol.Optional(CONF_NAME): str,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
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
|
vol.Optional(CONF_DOMAIN): vol.Any("light", "switch"),
|
||||||
}),
|
vol.Optional(
|
||||||
vol.Optional(CONF_DOMAIN): vol.Any('light', 'switch'),
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT, description='единицы измерений, либо строка либо мепинг'):
|
description="единицы измерений, либо строка либо мепинг",
|
||||||
vol.Any(str, {
|
): vol.Any(str, {vol.Required(str): str}),
|
||||||
vol.Required(str): str
|
vol.Optional(CONF_DEVICE_CLASS): vol.Any(str, {vol.Required(str): str}),
|
||||||
}),
|
|
||||||
vol.Optional(CONF_DEVICE_CLASS):
|
|
||||||
vol.Any(str, {
|
|
||||||
vol.Required(str): str
|
|
||||||
}),
|
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_RESPONSE_TEMPLATE,
|
CONF_RESPONSE_TEMPLATE,
|
||||||
description='шаблон ответа когда на этот порт приходит'
|
description="шаблон ответа когда на этот порт приходит" "сообщение из меги ",
|
||||||
'сообщение из меги '): cv.template,
|
): cv.template,
|
||||||
vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано
|
vol.Optional(CONF_ACTION): cv.script_action, # пока не реализовано
|
||||||
vol.Optional(CONF_GET_VALUE, default=True): bool,
|
vol.Optional(CONF_GET_VALUE, default=True): bool,
|
||||||
vol.Optional(CONF_CONV_TEMPLATE): cv.template,
|
vol.Optional(CONF_CONV_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
vol.Optional(CONF_FORCE_I2C_SCAN): bool,
|
vol.Optional(CONF_FORCE_I2C_SCAN): bool,
|
||||||
|
vol.Optional(CONF_HEX_TO_FLOAT): bool,
|
||||||
|
vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)],
|
||||||
|
vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_SMOOTH): cv.time_period_seconds,
|
||||||
|
# vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений
|
||||||
|
vol.Optional(str): {
|
||||||
|
vol.Optional(CONF_NAME): str,
|
||||||
|
vol.Optional(CONF_DEVICE_CLASS): str,
|
||||||
|
vol.Optional(CONF_UNIT_OF_MEASUREMENT): str,
|
||||||
|
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
CUSTOMIZE_DS2413 = {
|
CUSTOMIZE_DS2413 = {
|
||||||
vol.Optional(str.lower, description='адрес и индекс устройства'): CUSTOMIZE_PORT
|
vol.Optional(str.lower, description="адрес и индекс устройства"): CUSTOMIZE_PORT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def extender(x):
|
def extender(x):
|
||||||
if isinstance(x, str) and 'e' in x:
|
if isinstance(x, str) and "e" in x:
|
||||||
return x
|
return x
|
||||||
else:
|
else:
|
||||||
raise ValueError('must has "e" in port name')
|
raise ValueError('must has "e" in port name')
|
||||||
|
|
||||||
|
|
||||||
|
OWBUS = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_PORT): vol.Any(vol.Coerce(int), vol.Coerce(str)),
|
||||||
|
vol.Required(CONF_MEGA_ID): vol.Coerce(str),
|
||||||
|
vol.Required(CONF_ADDR): [str],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
DOMAIN: {
|
DOMAIN: {
|
||||||
vol.Optional(CONF_ALLOW_HOSTS): [str],
|
vol.Optional(CONF_ALLOW_HOSTS): [str],
|
||||||
vol.Required(str, description='id меги из веб-интерфейса'): {
|
vol.Optional("entities"): {
|
||||||
vol.Optional(CONF_FORCE_D, description='Принудительно слать d после срабатывания входа', default=False): bool,
|
vol.Optional(str): vol.Any(CUSTOMIZE_PORT, CUSTOMIZE_DS2413)
|
||||||
|
},
|
||||||
|
vol.Optional(vol.Any(str, int), description="id меги из веб-интерфейса"): {
|
||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_DEF_RESPONSE,
|
CONF_FORCE_D,
|
||||||
description='Ответ по умолчанию',
|
description="Принудительно слать d после срабатывания входа",
|
||||||
default=None
|
default=False,
|
||||||
|
): bool,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_DEF_RESPONSE, description="Ответ по умолчанию", default=None
|
||||||
): vol.Any(cv.template, None),
|
): vol.Any(cv.template, None),
|
||||||
vol.Optional(vol.Any(int, extender), description='номер порта'): vol.Any(
|
vol.Optional(CONF_LED): LED_LIGHT,
|
||||||
|
vol.Optional(
|
||||||
|
vol.Any(int, extender), description="номер порта"
|
||||||
|
): vol.Any(
|
||||||
CUSTOMIZE_PORT,
|
CUSTOMIZE_PORT,
|
||||||
CUSTOMIZE_DS2413,
|
CUSTOMIZE_DS2413,
|
||||||
)
|
),
|
||||||
}
|
vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)],
|
||||||
|
vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
|
||||||
|
vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
|
||||||
|
},
|
||||||
|
vol.Optional(CONF_1WBUS): [OWBUS],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
ALIVE_STATE = 'alive'
|
ALIVE_STATE = "alive"
|
||||||
DEF_ID = 'def'
|
DEF_ID = "def"
|
||||||
_POLL_TASKS = {}
|
_POLL_TASKS = {}
|
||||||
_hubs = {}
|
_hubs = {}
|
||||||
_subs = {}
|
_subs = {}
|
||||||
@@ -96,50 +194,50 @@ async def async_setup(hass: HomeAssistant, config: dict):
|
|||||||
view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
|
view.allowed_hosts |= set(config.get(DOMAIN, {}).get(CONF_ALLOW_HOSTS, []))
|
||||||
hass.http.register_view(view)
|
hass.http.register_view(view)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, 'save', partial(_save_service, hass), schema=vol.Schema({
|
DOMAIN,
|
||||||
vol.Optional('mega_id'): str
|
"save",
|
||||||
})
|
partial(_save_service, hass),
|
||||||
|
schema=vol.Schema({vol.Optional("mega_id"): str}),
|
||||||
)
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, 'get_port', partial(_get_port, hass), schema=vol.Schema({
|
DOMAIN,
|
||||||
vol.Optional('mega_id'): str,
|
"get_port",
|
||||||
vol.Optional('port'): vol.Any(int, [int]),
|
partial(_get_port, hass),
|
||||||
})
|
schema=vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Optional("mega_id"): str,
|
||||||
|
vol.Optional("port"): vol.Any(int, [int]),
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
DOMAIN, 'run_cmd', partial(_run_cmd, hass), schema=vol.Schema({
|
DOMAIN,
|
||||||
vol.Optional('port'): int,
|
"run_cmd",
|
||||||
vol.Required('cmd'): str,
|
partial(_run_cmd, hass),
|
||||||
vol.Optional('mega_id'): str,
|
schema=vol.Schema(
|
||||||
})
|
{
|
||||||
|
vol.Optional("port"): int,
|
||||||
|
vol.Required("cmd"): str,
|
||||||
|
vol.Optional("mega_id"): str,
|
||||||
|
}
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def get_hub(hass, entry):
|
async def get_hub(hass, entry):
|
||||||
id = entry.data.get('id', entry.entry_id)
|
id = entry.data.get("id", entry.entry_id)
|
||||||
data = dict(entry.data)
|
data = dict(entry.data)
|
||||||
data.update(entry.options or {})
|
data.update(entry.options or {})
|
||||||
data.update(id=id)
|
data.update(id=id)
|
||||||
use_mqtt = data.get(CONF_MQTT_INPUTS, True)
|
hub = MegaD(hass, config=entry, **data, lg=_LOGGER, loop=asyncio.get_event_loop())
|
||||||
|
|
||||||
_mqtt = hass.data.get(mqtt.DOMAIN) if use_mqtt else None
|
|
||||||
if _mqtt is None and use_mqtt:
|
|
||||||
for x in range(5):
|
|
||||||
await asyncio.sleep(5)
|
|
||||||
_mqtt = hass.data.get(mqtt.DOMAIN)
|
|
||||||
if _mqtt is not None:
|
|
||||||
break
|
|
||||||
if _mqtt is None:
|
|
||||||
raise Exception('mqtt not configured, please configure mqtt first')
|
|
||||||
hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER, loop=asyncio.get_event_loop())
|
|
||||||
hub.mqtt_id = await hub.get_mqtt_id()
|
hub.mqtt_id = await hub.get_mqtt_id()
|
||||||
return hub
|
return hub
|
||||||
|
|
||||||
|
|
||||||
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
|
async def _add_mega(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
id = entry.data.get('id', entry.entry_id)
|
id = entry.data.get("id", entry.entry_id)
|
||||||
hub = await get_hub(hass, entry)
|
hub = await get_hub(hass, entry)
|
||||||
hub.fw = await hub.get_fw()
|
hub.fw = await hub.get_fw()
|
||||||
hass.data[DOMAIN][id] = hub
|
hass.data[DOMAIN][id] = hub
|
||||||
@@ -158,9 +256,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
await hub.start()
|
await hub.start()
|
||||||
for platform in PLATFORMS:
|
for platform in PLATFORMS:
|
||||||
hass.async_create_task(
|
hass.async_create_task(
|
||||||
hass.config_entries.async_forward_entry_setup(
|
hass.config_entries.async_forward_entry_setup(entry, platform)
|
||||||
entry, platform
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
await hub.updater.async_refresh()
|
await hub.updater.async_refresh()
|
||||||
return True
|
return True
|
||||||
@@ -173,45 +269,51 @@ async def updater(hass: HomeAssistant, entry: ConfigEntry):
|
|||||||
:param entry:
|
:param entry:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
|
# hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
|
||||||
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
|
# hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
|
||||||
hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
|
# hub.port_to_scan = entry.options.get(CONF_PORT_TO_SCAN, 0)
|
||||||
entry.data = entry.options
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
for platform in PLATFORMS:
|
|
||||||
await hass.config_entries.async_forward_entry_unload(entry, platform)
|
|
||||||
await async_remove_entry(hass, entry)
|
|
||||||
await async_setup_entry(hass, entry)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_remove_entry(hass, entry) -> None:
|
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||||
"""Handle removal of an entry."""
|
"""Handle removal of an entry."""
|
||||||
id = entry.data.get('id', entry.entry_id)
|
id = entry.data.get("id", entry.entry_id)
|
||||||
hub: MegaD = hass.data[DOMAIN].get(id)
|
hub: MegaD = hass.data[DOMAIN].get(id)
|
||||||
if hub is None:
|
if hub is None:
|
||||||
return
|
return True
|
||||||
_LOGGER.debug(f'remove {id}')
|
_LOGGER.debug(f"remove {id}")
|
||||||
_hubs.pop(id, None)
|
_hubs.pop(id, None)
|
||||||
hass.data[DOMAIN].pop(id, None)
|
hass.data[DOMAIN].pop(id, None)
|
||||||
hass.data[DOMAIN][CONF_ALL].pop(id, None)
|
hass.data[DOMAIN][CONF_ALL].pop(id, None)
|
||||||
|
for platform in PLATFORMS:
|
||||||
|
await hass.config_entries.async_forward_entry_unload(entry, platform)
|
||||||
task: asyncio.Task = _POLL_TASKS.pop(id, None)
|
task: asyncio.Task = _POLL_TASKS.pop(id, None)
|
||||||
if task is not None:
|
if task is not None:
|
||||||
task.cancel()
|
task.cancel()
|
||||||
if hub is None:
|
if hub is None:
|
||||||
return
|
return True
|
||||||
await hub.stop()
|
await hub.stop()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async_unload_entry = async_remove_entry
|
||||||
|
|
||||||
|
|
||||||
async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
||||||
"""Migrate old entry."""
|
"""Migrate old entry."""
|
||||||
_LOGGER.debug("Migrating from version %s to version %s", config_entry.version, ConfigFlow.VERSION)
|
_LOGGER.debug(
|
||||||
|
"Migrating from version %s to version %s",
|
||||||
|
config_entry.version,
|
||||||
|
ConfigFlow.VERSION,
|
||||||
|
)
|
||||||
hub = await get_hub(hass, config_entry)
|
hub = await get_hub(hass, config_entry)
|
||||||
new = dict(config_entry.data)
|
new = dict(config_entry.data)
|
||||||
await hub.start()
|
await hub.start()
|
||||||
cfg = await hub.get_config()
|
cfg = await hub.get_config()
|
||||||
await hub.stop()
|
await hub.stop()
|
||||||
new.update(cfg)
|
new.update(cfg)
|
||||||
_LOGGER.debug(f'new config: %s', new)
|
_LOGGER.debug(f"new config: %s", new)
|
||||||
config_entry.data = new
|
config_entry.data = new
|
||||||
config_entry.version = ConfigFlow.VERSION
|
config_entry.version = ConfigFlow.VERSION
|
||||||
|
|
||||||
@@ -221,7 +323,7 @@ async def async_migrate_entry(hass, config_entry: ConfigEntry):
|
|||||||
|
|
||||||
|
|
||||||
async def _save_service(hass: HomeAssistant, call: ServiceCall):
|
async def _save_service(hass: HomeAssistant, call: ServiceCall):
|
||||||
mega_id = call.data.get('mega_id')
|
mega_id = call.data.get("mega_id")
|
||||||
if mega_id:
|
if mega_id:
|
||||||
hub: MegaD = hass.data[DOMAIN][mega_id]
|
hub: MegaD = hass.data[DOMAIN][mega_id]
|
||||||
await hub.save()
|
await hub.save()
|
||||||
@@ -233,8 +335,8 @@ async def _save_service(hass: HomeAssistant, call: ServiceCall):
|
|||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def _get_port(hass: HomeAssistant, call: ServiceCall):
|
async def _get_port(hass: HomeAssistant, call: ServiceCall):
|
||||||
port = call.data.get('port')
|
port = call.data.get("port")
|
||||||
mega_id = call.data.get('mega_id')
|
mega_id = call.data.get("mega_id")
|
||||||
if mega_id:
|
if mega_id:
|
||||||
hub: MegaD = hass.data[DOMAIN][mega_id]
|
hub: MegaD = hass.data[DOMAIN][mega_id]
|
||||||
if port is None:
|
if port is None:
|
||||||
@@ -244,6 +346,7 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
|
|||||||
elif isinstance(port, list):
|
elif isinstance(port, list):
|
||||||
for x in port:
|
for x in port:
|
||||||
await hub.get_port(x)
|
await hub.get_port(x)
|
||||||
|
hub.updater.async_set_updated_data(hub.values)
|
||||||
else:
|
else:
|
||||||
for hub in hass.data[DOMAIN][CONF_ALL].values():
|
for hub in hass.data[DOMAIN][CONF_ALL].values():
|
||||||
if not isinstance(hub, MegaD):
|
if not isinstance(hub, MegaD):
|
||||||
@@ -255,12 +358,13 @@ async def _get_port(hass: HomeAssistant, call: ServiceCall):
|
|||||||
elif isinstance(port, list):
|
elif isinstance(port, list):
|
||||||
for x in port:
|
for x in port:
|
||||||
await hub.get_port(x)
|
await hub.get_port(x)
|
||||||
|
hub.updater.async_set_updated_data(hub.values)
|
||||||
|
|
||||||
|
|
||||||
@bind_hass
|
@bind_hass
|
||||||
async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
|
async def _run_cmd(hass: HomeAssistant, call: ServiceCall):
|
||||||
mega_id = call.data.get('mega_id')
|
mega_id = call.data.get("mega_id")
|
||||||
cmd = call.data.get('cmd')
|
cmd = call.data.get("cmd")
|
||||||
if mega_id:
|
if mega_id:
|
||||||
hub: MegaD = hass.data[DOMAIN][mega_id]
|
hub: MegaD = hass.data[DOMAIN][mega_id]
|
||||||
await hub.request(cmd=cmd)
|
await hub.request(cmd=cmd)
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.mega.binary_sensors.append(self.port)
|
||||||
self._is_on = None
|
self._is_on = None
|
||||||
self._attrs = None
|
self._attrs = None
|
||||||
self._click_task = None
|
self._click_task = None
|
||||||
@@ -97,20 +98,4 @@ class MegaBinarySensor(BinarySensorEntity, MegaPushEntity):
|
|||||||
|
|
||||||
def _update(self, payload: dict):
|
def _update(self, payload: dict):
|
||||||
self.mega.values[self.port] = payload
|
self.mega.values[self.port] = payload
|
||||||
if not self.mega.mqtt_inputs:
|
|
||||||
return
|
|
||||||
|
|
||||||
template: Template = self.customize.get(CONF_RESPONSE_TEMPLATE, None)
|
|
||||||
if template is not None:
|
|
||||||
template.hass = self.hass
|
|
||||||
ret = template.async_render(payload)
|
|
||||||
self.mega.lg.debug(f'response: %s', ret)
|
|
||||||
self.hass.async_create_task(
|
|
||||||
self.mega.request(pt=self.port, cmd=ret)
|
|
||||||
)
|
|
||||||
elif self.mega.force_d:
|
|
||||||
self.mega.lg.debug(f'response d')
|
|
||||||
self.hass.async_create_task(
|
|
||||||
self.mega.request(pt=self.port, cmd='d')
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ import logging
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, core
|
from homeassistant import config_entries, core
|
||||||
from homeassistant.components import mqtt
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
|
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
|
||||||
from homeassistant.core import callback, HomeAssistant
|
from homeassistant.core import callback, HomeAssistant
|
||||||
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, PLATFORMS, CONF_MQTT_INPUTS, \
|
from .const import DOMAIN, CONF_RELOAD, \
|
||||||
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
|
CONF_NPORTS, CONF_UPDATE_ALL, CONF_POLL_OUTS, CONF_FAKE_RESPONSE, CONF_FORCE_D, \
|
||||||
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME, \
|
CONF_ALLOW_HOSTS, CONF_PROTECTED, CONF_RESTORE_ON_RESTART, CONF_UPDATE_TIME
|
||||||
REMOVE_CONFIG # pylint:disable=unused-import
|
|
||||||
from .hub import MegaD
|
from .hub import MegaD
|
||||||
from . import exceptions
|
from . import exceptions
|
||||||
|
|
||||||
@@ -23,10 +21,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
vol.Required(CONF_ID, default='mega'): str,
|
vol.Required(CONF_ID, default='mega'): str,
|
||||||
vol.Required(CONF_HOST, default="192.168.0.14"): str,
|
vol.Required(CONF_HOST, default="192.168.0.14"): str,
|
||||||
vol.Required(CONF_PASSWORD, default="sec"): str,
|
vol.Required(CONF_PASSWORD, default="sec"): str,
|
||||||
vol.Optional(CONF_SCAN_INTERVAL, default=0): int,
|
vol.Optional(CONF_SCAN_INTERVAL, default=30): int,
|
||||||
vol.Optional(CONF_POLL_OUTS, default=False): bool,
|
vol.Optional(CONF_POLL_OUTS, default=False): bool,
|
||||||
# vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
|
# vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
|
||||||
vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
|
# vol.Optional(CONF_MQTT_INPUTS, default=False): bool,
|
||||||
vol.Optional(CONF_NPORTS, default=37): int,
|
vol.Optional(CONF_NPORTS, default=37): int,
|
||||||
vol.Optional(CONF_UPDATE_ALL, default=True): bool,
|
vol.Optional(CONF_UPDATE_ALL, default=True): bool,
|
||||||
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
|
vol.Optional(CONF_FAKE_RESPONSE, default=True): bool,
|
||||||
@@ -40,10 +38,10 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
|||||||
|
|
||||||
|
|
||||||
async def get_hub(hass: HomeAssistant, data):
|
async def get_hub(hass: HomeAssistant, data):
|
||||||
_mqtt = hass.data.get(mqtt.DOMAIN)
|
# _mqtt = hass.data.get(mqtt.DOMAIN)
|
||||||
# if not isinstance(_mqtt, mqtt.MQTT):
|
# if not isinstance(_mqtt, mqtt.MQTT):
|
||||||
# raise exceptions.MqttNotConfigured("mqtt must be configured first")
|
# raise exceptions.MqttNotConfigured("mqtt must be configured first")
|
||||||
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt, loop=asyncio.get_event_loop())
|
hub = MegaD(hass, **data, lg=_LOGGER, loop=asyncio.get_event_loop()) #mqtt=_mqtt,
|
||||||
hub.mqtt_id = await hub.get_mqtt_id()
|
hub.mqtt_id = await hub.get_mqtt_id()
|
||||||
if not await hub.authenticate():
|
if not await hub.authenticate():
|
||||||
raise exceptions.InvalidAuth
|
raise exceptions.InvalidAuth
|
||||||
@@ -65,7 +63,7 @@ async def validate_input(hass: core.HomeAssistant, data):
|
|||||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for mega."""
|
"""Handle a config flow for mega."""
|
||||||
|
|
||||||
VERSION = 20
|
VERSION = 26
|
||||||
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
|
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
|
||||||
|
|
||||||
async def async_step_user(self, user_input=None):
|
async def async_step_user(self, user_input=None):
|
||||||
@@ -80,6 +78,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
hub = await validate_input(self.hass, user_input)
|
hub = await validate_input(self.hass, user_input)
|
||||||
await hub.start()
|
await hub.start()
|
||||||
|
hub.new_naming=True
|
||||||
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
|
config = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
|
||||||
await hub.stop()
|
await hub.stop()
|
||||||
hub.lg.debug(f'config loaded: %s', config)
|
hub.lg.debug(f'config loaded: %s', config)
|
||||||
@@ -117,26 +116,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
async def async_step_init(self, user_input=None):
|
async def async_step_init(self, user_input=None):
|
||||||
"""Manage the options."""
|
"""Manage the options."""
|
||||||
new_naming = self.config_entry.data.get('new_naming', False)
|
new_naming = self.config_entry.data.get('new_naming', False)
|
||||||
hub = await get_hub(self.hass, self.config_entry.data)
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
reload = user_input.pop(CONF_RELOAD)
|
reload = user_input.pop(CONF_RELOAD)
|
||||||
cfg = dict(self.config_entry.data)
|
cfg = dict(self.config_entry.data)
|
||||||
cfg.update(user_input)
|
cfg.update(user_input)
|
||||||
hub = await get_hub(self.hass, cfg)
|
|
||||||
if reload:
|
|
||||||
await hub.start()
|
|
||||||
new = await hub.get_config(nports=user_input.get(CONF_NPORTS, 37))
|
|
||||||
await hub.stop()
|
|
||||||
|
|
||||||
_LOGGER.debug(f'new config: %s', new)
|
|
||||||
cfg = dict(self.config_entry.data)
|
|
||||||
for x in PLATFORMS:
|
|
||||||
cfg.pop(x, None)
|
|
||||||
for x in REMOVE_CONFIG:
|
|
||||||
cfg.pop(x, default=None)
|
|
||||||
cfg.update(new)
|
|
||||||
|
|
||||||
cfg['new_naming'] = new_naming
|
cfg['new_naming'] = new_naming
|
||||||
|
self.config_entry.data = cfg
|
||||||
|
await get_hub(self.hass, cfg)
|
||||||
|
|
||||||
|
if reload:
|
||||||
|
id = self.config_entry.data.get('id', self.config_entry.entry_id)
|
||||||
|
hub: MegaD = self.hass.data[DOMAIN].get(id)
|
||||||
|
cfg = await hub.reload(reload_entry=False)
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title='',
|
title='',
|
||||||
data=cfg,
|
data=cfg,
|
||||||
@@ -148,7 +140,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
|
vol.Optional(CONF_SCAN_INTERVAL, default=e.get(CONF_SCAN_INTERVAL, 0)): int,
|
||||||
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
|
vol.Optional(CONF_POLL_OUTS, default=e.get(CONF_POLL_OUTS, False)): bool,
|
||||||
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
|
# vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
|
||||||
vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
|
# vol.Optional(CONF_MQTT_INPUTS, default=e.get(CONF_MQTT_INPUTS, True)): bool,
|
||||||
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
|
vol.Optional(CONF_NPORTS, default=e.get(CONF_NPORTS, 37)): int,
|
||||||
vol.Optional(CONF_RELOAD, default=False): bool,
|
vol.Optional(CONF_RELOAD, default=False): bool,
|
||||||
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
|
vol.Optional(CONF_UPDATE_ALL, default=e.get(CONF_UPDATE_ALL, True)): bool,
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ def parse_config(page: str):
|
|||||||
v = page.find('input', attrs={'name': x})
|
v = page.find('input', attrs={'name': x})
|
||||||
if v:
|
if v:
|
||||||
ret[x] = v['value']
|
ret[x] = v['value']
|
||||||
|
smooth = page.find('input', attrs={'name': 'misc'})
|
||||||
|
if smooth is None or smooth.get('checked') is None:
|
||||||
|
ret['misc'] = None
|
||||||
return Config(**ret, src=page)
|
return Config(**ret, src=page)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""Constants for the mega integration."""
|
"""Constants for the mega integration."""
|
||||||
import re
|
import re
|
||||||
|
from itertools import permutations
|
||||||
|
|
||||||
DOMAIN = "mega"
|
DOMAIN = "mega"
|
||||||
CONF_MEGA_ID = "mega_id"
|
CONF_MEGA_ID = "mega_id"
|
||||||
@@ -36,6 +37,21 @@ CONF_CLICK_TIME = 'click_time'
|
|||||||
CONF_LONG_TIME = 'long_time'
|
CONF_LONG_TIME = 'long_time'
|
||||||
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
|
CONF_FORCE_I2C_SCAN = 'force_i2c_scan'
|
||||||
CONF_UPDATE_TIME = 'update_time'
|
CONF_UPDATE_TIME = 'update_time'
|
||||||
|
CONF_HEX_TO_FLOAT = 'hex_to_float'
|
||||||
|
CONF_LED = 'led'
|
||||||
|
CONF_WS28XX = 'ws28xx'
|
||||||
|
CONF_ORDER = 'order'
|
||||||
|
CONF_SMOOTH = 'smooth'
|
||||||
|
CONF_WHITE_SEP = 'white_sep'
|
||||||
|
CONF_CHIP = 'chip'
|
||||||
|
CONF_RANGE = 'range'
|
||||||
|
CONF_FILL_NA = 'fill_na'
|
||||||
|
CONF_FILTER_VALUES = 'filter_values'
|
||||||
|
CONF_FILTER_SCALE = 'filter_scale'
|
||||||
|
CONF_FILTER_LOW = 'filter_low'
|
||||||
|
CONF_FILTER_HIGH = 'filter_high'
|
||||||
|
CONF_1WBUS = '1wbus'
|
||||||
|
CONF_ADDR = 'addr'
|
||||||
PLATFORMS = [
|
PLATFORMS = [
|
||||||
"light",
|
"light",
|
||||||
"switch",
|
"switch",
|
||||||
@@ -55,7 +71,7 @@ LUX = 'lux'
|
|||||||
SINGLE_CLICK = 'single'
|
SINGLE_CLICK = 'single'
|
||||||
DOUBLE_CLICK = 'double'
|
DOUBLE_CLICK = 'double'
|
||||||
|
|
||||||
PATT_FW = re.compile(r'fw:\s(.+)\)')
|
PATT_FW = re.compile(r'fw:\s(.+?)\)')
|
||||||
|
|
||||||
REMOVE_CONFIG = [
|
REMOVE_CONFIG = [
|
||||||
'extenders',
|
'extenders',
|
||||||
@@ -66,4 +82,7 @@ REMOVE_CONFIG = [
|
|||||||
'light',
|
'light',
|
||||||
'i2c',
|
'i2c',
|
||||||
'sensor',
|
'sensor',
|
||||||
]
|
'smooth',
|
||||||
|
]
|
||||||
|
RGB_COMBINATIONS = [''.join(x) for x in permutations('rgb')]
|
||||||
|
RGB = 'rgb'
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
import logging
|
import logging
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
|
import typing
|
||||||
|
from datetime import timedelta
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
from homeassistant.core import State
|
from homeassistant.core import State
|
||||||
|
from homeassistant.helpers.entity import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
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
|
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__)
|
||||||
@@ -38,7 +43,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
mega: 'h.MegaD',
|
mega: 'h.MegaD',
|
||||||
port: int,
|
port: typing.Union[int, str, typing.List[int]],
|
||||||
config_entry: ConfigEntry = None,
|
config_entry: ConfigEntry = None,
|
||||||
id_suffix=None,
|
id_suffix=None,
|
||||||
name=None,
|
name=None,
|
||||||
@@ -46,11 +51,12 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
http_cmd='get',
|
http_cmd='get',
|
||||||
addr: str=None,
|
addr: str=None,
|
||||||
index=None,
|
index=None,
|
||||||
|
customize=None,
|
||||||
|
smooth=None,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
super().__init__(mega.updater)
|
self._smooth = smooth
|
||||||
|
|
||||||
self.http_cmd = http_cmd
|
self.http_cmd = http_cmd
|
||||||
|
|
||||||
self._state: State = None
|
self._state: State = None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
@@ -58,16 +64,57 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
mega.entities.append(self)
|
mega.entities.append(self)
|
||||||
self._mega_id = mega.id
|
self._mega_id = mega.id
|
||||||
self._lg = None
|
self._lg = None
|
||||||
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
|
if not isinstance(port, list):
|
||||||
(f"_{id_suffix}" if id_suffix else "")
|
self._unique_id = unique_id or f"mega_{mega.id}_{port}" + \
|
||||||
_pt = port if not mega.new_naming else f'{port:02}'
|
(f"_{id_suffix}" if id_suffix else "")
|
||||||
self._name = name or f"{mega.id}_{_pt}" + \
|
_pt = port if not mega.new_naming else f'{port:02}' if isinstance(port, int) else port
|
||||||
(f"_{id_suffix}" if id_suffix else "")
|
self._name = name or f"{mega.id}_{_pt}" + \
|
||||||
self._customize: dict = None
|
(f"_{id_suffix}" if id_suffix else "")
|
||||||
|
self._customize: dict = None
|
||||||
|
else:
|
||||||
|
assert id_suffix is not None
|
||||||
|
assert name is not None
|
||||||
|
assert isinstance(customize, dict)
|
||||||
|
self._unique_id = unique_id or f"mega_{mega.id}_{id_suffix}"
|
||||||
|
self._name = name
|
||||||
|
self._customize = customize
|
||||||
|
|
||||||
self.index = index
|
self.index = index
|
||||||
self.addr = addr
|
self.addr = addr
|
||||||
|
self.id_suffix = id_suffix
|
||||||
|
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
|
||||||
|
def is_ws(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_attribute(self, name, default=None):
|
||||||
|
attr = getattr(self, f'_{name}', None)
|
||||||
|
if attr is None and self._state is not None:
|
||||||
|
if name == 'is_on':
|
||||||
|
attr = self._state.state
|
||||||
|
else:
|
||||||
|
attr = self._state.attributes.get(f'{name}', default)
|
||||||
|
return attr if attr is not None else default
|
||||||
|
|
||||||
|
@property
|
||||||
|
def can_smooth_hardware(self):
|
||||||
|
if self._can_smooth_hard is None:
|
||||||
|
if self.is_ws:
|
||||||
|
self._can_smooth_hard = False
|
||||||
|
if not isinstance(self.port, list):
|
||||||
|
self._can_smooth_hard = self.port in self.mega.smooth
|
||||||
|
else:
|
||||||
|
for x in self.port:
|
||||||
|
if isinstance(x, str):
|
||||||
|
self._can_smooth_hard = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self._can_smooth_hard = self.port in self.mega.smooth
|
||||||
|
return self._can_smooth_hard
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enabled(self):
|
def enabled(self):
|
||||||
@@ -78,42 +125,47 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def customize(self):
|
def customize(self):
|
||||||
if self.hass is None:
|
if self._customize is not None:
|
||||||
|
return self._customize
|
||||||
|
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(CONF_CUSTOM).get('entities', {}).get(self.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 {}
|
||||||
if self.addr is not None and self.index is not None and isinstance(c, dict):
|
if self.addr is not None and self.index is not None and isinstance(c, dict):
|
||||||
idx = self.addr.lower() + f'_a' if self.index == 0 else '_b'
|
idx = self.addr.lower() + f'_a' if self.index == 0 else '_b'
|
||||||
c = c.get(idx, {})
|
c = c.get(idx, {})
|
||||||
|
c.update(c_entity_id)
|
||||||
self._customize = c
|
self._customize = c
|
||||||
|
|
||||||
return self._customize
|
return self._customize
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_info(self):
|
def device_info(self) -> DeviceInfo:
|
||||||
_pt = self.port if not self.mega.new_naming else f'{self.port:02}'
|
if isinstance(self.port, list):
|
||||||
return {
|
pt_idx = self.id_suffix
|
||||||
"identifiers": {
|
else:
|
||||||
|
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
|
||||||
|
if isinstance(_pt, str) and 'e' in _pt:
|
||||||
|
pt_idx, _ = _pt.split('e')
|
||||||
|
else:
|
||||||
|
pt_idx = _pt
|
||||||
|
return DeviceInfo(
|
||||||
|
identifiers={
|
||||||
# Serial numbers are unique identifiers within a specific domain
|
# Serial numbers are unique identifiers within a specific domain
|
||||||
(DOMAIN, f'{self._mega_id}', self.port),
|
(DOMAIN, f'{self._mega_id}', pt_idx)
|
||||||
},
|
},
|
||||||
"config_entries": [
|
name=self.name,
|
||||||
self.config_entry,
|
manufacturer='ab-log.ru',
|
||||||
],
|
sw_version=self.mega.fw,
|
||||||
"name": f'{self._mega_id} port {_pt}',
|
via_device=(DOMAIN, self._mega_id),
|
||||||
"manufacturer": 'ab-log.ru',
|
)
|
||||||
# "model": self.light.productname,
|
|
||||||
"sw_version": self.mega.fw,
|
|
||||||
"via_device": (DOMAIN, self._mega_id),
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def lg(self) -> logging.Logger:
|
def lg(self) -> logging.Logger:
|
||||||
if self._lg is None:
|
return _LOGGER
|
||||||
self._lg = self.mega.lg.getChild(self._name or self.unique_id)
|
|
||||||
return self._lg
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
@@ -123,8 +175,11 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
def name(self):
|
def name(self):
|
||||||
c = self.customize.get(CONF_NAME)
|
c = self.customize.get(CONF_NAME)
|
||||||
if not isinstance(c, str):
|
if not isinstance(c, str):
|
||||||
_pt = self.port if not self.mega.new_naming else f'{self.port:02}'
|
if not isinstance(self.port, list):
|
||||||
c = self._name or f"{self.mega.id}_p{_pt}"
|
_pt = self.port if not self.mega.new_naming else f'{self.port:02}' if isinstance(self.port, int) else self.port
|
||||||
|
c = self._name or f"{self.mega.id}_p{_pt}"
|
||||||
|
else:
|
||||||
|
c = self.id_suffix
|
||||||
return c
|
return c
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -135,13 +190,10 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
|
|||||||
global _task_set_ev_on
|
global _task_set_ev_on
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self._state = await self.async_get_last_state()
|
self._state = await self.async_get_last_state()
|
||||||
if self.mega.mqtt_inputs and _task_set_ev_on is None:
|
|
||||||
_task_set_ev_on = asyncio.create_task(_set_events_on())
|
|
||||||
|
|
||||||
async def get_state(self):
|
async def get_state(self):
|
||||||
self.lg.debug(f'state is %s', self.state)
|
self.lg.debug(f'state is %s', self.state)
|
||||||
if not self.mega.mqtt_inputs:
|
self.async_write_ha_state()
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|
||||||
|
|
||||||
class MegaPushEntity(BaseMegaEntity):
|
class MegaPushEntity(BaseMegaEntity):
|
||||||
@@ -157,11 +209,10 @@ class MegaPushEntity(BaseMegaEntity):
|
|||||||
|
|
||||||
def __update(self, value: dict):
|
def __update(self, value: dict):
|
||||||
self._update(value)
|
self._update(value)
|
||||||
|
if self.hass is None:
|
||||||
|
return
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
self.lg.debug(f'state after update %s', self.state)
|
self.lg.debug(f'state after update %s', self.state)
|
||||||
if self.mega.mqtt_inputs and not _events_on:
|
|
||||||
_LOGGER.debug('skip event because events are off')
|
|
||||||
return
|
|
||||||
if not self.entity_id.startswith('binary_sensor'):
|
if not self.entity_id.startswith('binary_sensor'):
|
||||||
_LOGGER.debug('skip event because not a bnary sens')
|
_LOGGER.debug('skip event because not a bnary sens')
|
||||||
return
|
return
|
||||||
@@ -213,11 +264,6 @@ class MegaPushEntity(BaseMegaEntity):
|
|||||||
def _update(self, payload: dict):
|
def _update(self, payload: dict):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
|
||||||
await super().async_added_to_hass()
|
|
||||||
if self.mega.mqtt is not None:
|
|
||||||
asyncio.create_task(self.mega.get_port(self.port))
|
|
||||||
|
|
||||||
|
|
||||||
class MegaOutPort(MegaPushEntity):
|
class MegaOutPort(MegaPushEntity):
|
||||||
|
|
||||||
@@ -234,40 +280,61 @@ class MegaOutPort(MegaPushEntity):
|
|||||||
self._is_on = None
|
self._is_on = None
|
||||||
self.dimmer = dimmer
|
self.dimmer = dimmer
|
||||||
self.dimmer_scale = dimmer_scale
|
self.dimmer_scale = dimmer_scale
|
||||||
|
self.is_extender = isinstance(self.port, str) and 'e' in self.port
|
||||||
|
self.task: asyncio.Task = None
|
||||||
|
self._restore_brightness = None
|
||||||
|
self._last_called: float = 0
|
||||||
|
|
||||||
# @property
|
# @property
|
||||||
# def assumed_state(self) -> bool:
|
# def assumed_state(self) -> bool:
|
||||||
# return True if self.index is not None or self.mega.mqtt is None else False
|
# return True if self.index is not None or self.mega.mqtt is None else False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_dim(self):
|
||||||
|
if self.dimmer_scale == 1:
|
||||||
|
return 255
|
||||||
|
elif self.dimmer_scale == 16:
|
||||||
|
return 4095
|
||||||
|
else:
|
||||||
|
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:
|
||||||
@@ -316,19 +383,97 @@ class MegaOutPort(MegaPushEntity):
|
|||||||
else:
|
else:
|
||||||
return self.port
|
return self.port
|
||||||
|
|
||||||
async def async_turn_on(self, brightness=None, **kwargs) -> None:
|
@property
|
||||||
|
def smooth(self) -> timedelta:
|
||||||
|
ret = self.customize.get(CONF_SMOOTH)
|
||||||
|
if ret is None and self._smooth:
|
||||||
|
ret = timedelta(seconds=self._smooth)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def smooth_dim(self):
|
||||||
|
if not self.dimmer:
|
||||||
|
return False
|
||||||
|
return self.smooth or self.can_smooth_hardware
|
||||||
|
|
||||||
|
def update_from_smooth(self, value, update_state=False):
|
||||||
|
if isinstance(self.port, str):
|
||||||
|
self.mega.values[self.port] = value[0]
|
||||||
|
else:
|
||||||
|
self.mega.values[self.port] = {
|
||||||
|
'value': value[0]
|
||||||
|
}
|
||||||
|
if update_state:
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def _set_dim_brightness(self, from_, to_, transition):
|
||||||
|
pct = abs(to_ - from_) / (255 if self.dimmer_scale == 1 else 4095)
|
||||||
|
update_state = transition is not None and transition > 3
|
||||||
|
tm = (self.smooth.total_seconds() * pct) if transition is None else transition
|
||||||
|
if self.task is not None:
|
||||||
|
self.task.cancel()
|
||||||
|
self.task = asyncio.create_task(self.mega.smooth_dim(
|
||||||
|
(self.cmd_port, from_, to_),
|
||||||
|
time=tm,
|
||||||
|
can_smooth_hardware=self.can_smooth_hardware,
|
||||||
|
max_values=[255 if self.dimmer_scale == 1 else 4095],
|
||||||
|
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):
|
||||||
|
if (time.time() - self._last_called) < 0.1:
|
||||||
|
return
|
||||||
|
self._last_called = time.time()
|
||||||
|
if not self.dimmer:
|
||||||
|
transition = None
|
||||||
|
if not self.is_on and (brightness is None or brightness == 0):
|
||||||
|
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
|
||||||
self._brightness = brightness
|
self._brightness = brightness
|
||||||
if self.dimmer and brightness == 0:
|
if self.dimmer and brightness == 0:
|
||||||
cmd = 255 * self.dimmer_scale
|
cmd = self.max_dim
|
||||||
elif self.dimmer:
|
elif self.dimmer:
|
||||||
cmd = brightness * self.dimmer_scale
|
cmd = min((brightness * self.dimmer_scale, self.max_dim))
|
||||||
|
if self.smooth_dim or transition:
|
||||||
|
self._set_dim_brightness(from_=_prev, to_=cmd, transition=transition)
|
||||||
else:
|
else:
|
||||||
cmd = 1 if not self.invert else 0
|
cmd = 1 if not self.invert else 0
|
||||||
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
|
if transition is None:
|
||||||
|
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
|
||||||
|
else:
|
||||||
|
_cmd = {
|
||||||
|
"pt": f"{self.cmd_port}",
|
||||||
|
"pwm": cmd,
|
||||||
|
"cnt": round(transition / (abs(_prev - brightness) / 255)),
|
||||||
|
}
|
||||||
if self.addr:
|
if self.addr:
|
||||||
_cmd['addr'] = self.addr
|
_cmd['addr'] = self.addr
|
||||||
await self.mega.request(**_cmd, priority=-1)
|
if not (self.smooth_dim or transition):
|
||||||
|
await self.mega.request(**_cmd, priority=-1)
|
||||||
if self.index is not None:
|
if self.index is not None:
|
||||||
# обновление текущего стейта для ds2413
|
# обновление текущего стейта для ds2413
|
||||||
await self.mega.get_port(
|
await self.mega.get_port(
|
||||||
@@ -346,13 +491,26 @@ class MegaOutPort(MegaPushEntity):
|
|||||||
self.mega.values[self.port] = {'value': cmd}
|
self.mega.values[self.port] = {'value': cmd}
|
||||||
await self.get_state()
|
await self.get_state()
|
||||||
|
|
||||||
async def async_turn_off(self, **kwargs) -> None:
|
async def async_turn_off(self, transition=None, **kwargs) -> None:
|
||||||
|
if (time.time() - self._last_called) < 0.1:
|
||||||
|
return
|
||||||
|
self._last_called = time.time()
|
||||||
|
self._restore_brightness = self._cal_reverse_brightness(safe_int(self._brightness))
|
||||||
|
if not self.dimmer:
|
||||||
|
transition = None
|
||||||
cmd = "0" if not self.invert else "1"
|
cmd = "0" if not self.invert else "1"
|
||||||
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
|
_cmd = {"cmd": f"{self.cmd_port}:{cmd}"}
|
||||||
|
_prev = safe_int(self.brightness) or 0
|
||||||
if self.addr:
|
if self.addr:
|
||||||
_cmd['addr'] = self.addr
|
_cmd['addr'] = self.addr
|
||||||
await self.mega.request(**_cmd, priority=-1)
|
if not (self.smooth_dim or transition):
|
||||||
|
await self.mega.request(**_cmd, priority=-1)
|
||||||
|
else:
|
||||||
|
self._set_dim_brightness(
|
||||||
|
from_=_prev,
|
||||||
|
to_=0,
|
||||||
|
transition=transition,
|
||||||
|
)
|
||||||
if self.index is not None:
|
if self.index is not None:
|
||||||
# обновление текущего стейта для ds2413
|
# обновление текущего стейта для ds2413
|
||||||
await self.mega.get_port(
|
await self.mega.get_port(
|
||||||
@@ -367,13 +525,18 @@ class MegaOutPort(MegaPushEntity):
|
|||||||
self.mega.values[self.port] = {'value': cmd}
|
self.mega.values[self.port] = {'value': cmd}
|
||||||
await self.get_state()
|
await self.get_state()
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
if self.task is not None:
|
||||||
|
self.task.cancel()
|
||||||
|
|
||||||
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
|
||||||
@@ -5,10 +5,6 @@ class CannotConnect(exceptions.HomeAssistantError):
|
|||||||
"""Error to indicate we cannot connect."""
|
"""Error to indicate we cannot connect."""
|
||||||
|
|
||||||
|
|
||||||
class MqttNotConfigured(exceptions.HomeAssistantError):
|
|
||||||
"""Error to indicate mqtt is not configured"""
|
|
||||||
|
|
||||||
|
|
||||||
class DuplicateId(exceptions.HomeAssistantError):
|
class DuplicateId(exceptions.HomeAssistantError):
|
||||||
"""Error to indicate duplicate id"""
|
"""Error to indicate duplicate id"""
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,11 @@ class MegaView(HomeAssistantView):
|
|||||||
if hub is None:
|
if hub is None:
|
||||||
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
|
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
|
||||||
if hub is None and request.remote in ['::1', '127.0.0.1']:
|
if hub is None and request.remote in ['::1', '127.0.0.1']:
|
||||||
hub = self.hubs.get('__def')
|
try:
|
||||||
|
hub = list(self.hubs.values())[0]
|
||||||
|
except IndexError:
|
||||||
|
_LOGGER.warning(f'can not find mdid={request.query["mdid"]} in {list(self.hubs)}')
|
||||||
|
return Response(status=400)
|
||||||
elif hub is None:
|
elif hub is None:
|
||||||
return Response(status=400)
|
return Response(status=400)
|
||||||
data = dict(request.query)
|
data = dict(request.query)
|
||||||
@@ -87,8 +91,8 @@ class MegaView(HomeAssistantView):
|
|||||||
)
|
)
|
||||||
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
|
_LOGGER.debug(f"Request: %s from '%s'", data, request.remote)
|
||||||
make_ints(data)
|
make_ints(data)
|
||||||
if data.get('st') == '1' and hub.restore_on_restart:
|
if data.get('st') == '1':
|
||||||
asyncio.create_task(self.later_restore(hub))
|
hass.async_create_task(self.later_restore(hub))
|
||||||
return Response(status=200)
|
return Response(status=200)
|
||||||
port = data.get('pt')
|
port = data.get('pt')
|
||||||
data = data.copy()
|
data = data.copy()
|
||||||
@@ -113,7 +117,7 @@ class MegaView(HomeAssistantView):
|
|||||||
_data = data.copy()
|
_data = data.copy()
|
||||||
if e.startswith('ext'):
|
if e.startswith('ext'):
|
||||||
idx = e[3:]
|
idx = e[3:]
|
||||||
pt = f'{pt_orig}e{idx}'
|
pt = f'{pt_orig}e{idx}' if not hub.new_naming else f'{int(pt_orig):02d}e{int(idx):02d}'
|
||||||
_data['pt_orig'] = pt_orig
|
_data['pt_orig'] = pt_orig
|
||||||
_data['value'] = 'ON' if v == '1' else 'OFF'
|
_data['value'] = 'ON' if v == '1' else 'OFF'
|
||||||
_data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
|
_data['m'] = 1 if _data[e] == '0' else 0 # имитация поведения обычного входа, чтобы события обрабатывались аналогично
|
||||||
@@ -131,6 +135,7 @@ class MegaView(HomeAssistantView):
|
|||||||
await hub.request(cmd=act.replace(':3', f':{v}'))
|
await hub.request(cmd=act.replace(':3', f':{v}'))
|
||||||
ret = 'd' if hub.force_d else ''
|
ret = 'd' if hub.force_d else ''
|
||||||
else:
|
else:
|
||||||
|
# elif port in hub.binary_sensors:
|
||||||
hub.values[port] = data
|
hub.values[port] = data
|
||||||
for cb in self.callbacks[hub.id][port]:
|
for cb in self.callbacks[hub.id][port]:
|
||||||
cb(data)
|
cb(data)
|
||||||
@@ -143,7 +148,7 @@ class MegaView(HomeAssistantView):
|
|||||||
_LOGGER.debug('response %s', ret)
|
_LOGGER.debug('response %s', ret)
|
||||||
Response(body='' if hub.fake_response else ret, content_type='text/plain')
|
Response(body='' if hub.fake_response else ret, content_type='text/plain')
|
||||||
|
|
||||||
if hub.fake_response and 'value' not in data and 'pt' in data:
|
if hub.fake_response and 'value' not in data and 'pt' in data and port in hub.binary_sensors:
|
||||||
if 'd' in ret:
|
if 'd' in ret:
|
||||||
await hub.request(pt=port, cmd=ret)
|
await hub.request(pt=port, cmd=ret)
|
||||||
else:
|
else:
|
||||||
@@ -158,10 +163,11 @@ class MegaView(HomeAssistantView):
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
await asyncio.sleep(0.2)
|
await asyncio.sleep(0.2)
|
||||||
await hub.restore_states()
|
if hub.restore_on_restart:
|
||||||
|
await hub.restore_states()
|
||||||
|
await hub.reload()
|
||||||
|
|
||||||
async def later_update(self, hub):
|
async def later_update(self, hub):
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
_LOGGER.debug('force update')
|
_LOGGER.debug('force update')
|
||||||
await hub.updater.async_refresh()
|
await hub.updater.async_refresh()
|
||||||
|
|
||||||
@@ -9,50 +9,50 @@ import re
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from homeassistant.components import mqtt
|
|
||||||
from homeassistant.const import (
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
|
from homeassistant.config_entries import ConfigEntry
|
||||||
DEVICE_CLASS_ILLUMINANCE, TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
|
from homeassistant.const import TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
|
||||||
)
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||||
from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
|
from .config_parser import parse_config, DS2413, MCP230, MCP230_OUT, MCP230_IN, PCA9685
|
||||||
from .const import (
|
from .const import (
|
||||||
TEMP, HUM, PRESS,
|
TEMP,
|
||||||
LUX, PATT_SPLIT, DOMAIN,
|
HUM,
|
||||||
CONF_HTTP, EVENT_BINARY_SENSOR, CONF_CUSTOM, CONF_FORCE_D, CONF_DEF_RESPONSE, PATT_FW, CONF_FORCE_I2C_SCAN
|
PRESS,
|
||||||
|
LUX,
|
||||||
|
PATT_SPLIT,
|
||||||
|
DOMAIN,
|
||||||
|
CONF_HTTP,
|
||||||
|
EVENT_BINARY_SENSOR,
|
||||||
|
CONF_CUSTOM,
|
||||||
|
CONF_FORCE_D,
|
||||||
|
CONF_DEF_RESPONSE,
|
||||||
|
PATT_FW,
|
||||||
|
CONF_FORCE_I2C_SCAN,
|
||||||
|
REMOVE_CONFIG,
|
||||||
)
|
)
|
||||||
from .entities import set_events_off, BaseMegaEntity, MegaOutPort
|
from .entities import set_events_off, BaseMegaEntity, MegaOutPort, safe_int
|
||||||
from .exceptions import CannotConnect, NoPort
|
from .exceptions import CannotConnect, NoPort
|
||||||
from .i2c import parse_scan_page
|
from .i2c import parse_scan_page
|
||||||
from .tools import make_ints, int_ignore, PriorityLock
|
from .tools import make_ints, int_ignore, PriorityLock
|
||||||
|
|
||||||
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
|
TEMP_PATT = re.compile(r"temp:([01234567890\.]+)")
|
||||||
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
|
HUM_PATT = re.compile(r"hum:([01234567890\.]+)")
|
||||||
PRESS_PATT = re.compile(r'press:([01234567890\.]+)')
|
PRESS_PATT = re.compile(r"press:([01234567890\.]+)")
|
||||||
LUX_PATT = re.compile(r'lux:([01234567890\.]+)')
|
LUX_PATT = re.compile(r"lux:([01234567890\.]+)")
|
||||||
PATTERNS = {
|
PATTERNS = {TEMP: TEMP_PATT, HUM: HUM_PATT, PRESS: PRESS_PATT, LUX: LUX_PATT}
|
||||||
TEMP: TEMP_PATT,
|
UNITS = {TEMP: TEMP_CELSIUS, HUM: PERCENTAGE, PRESS: "mmHg", LUX: LIGHT_LUX}
|
||||||
HUM: HUM_PATT,
|
|
||||||
PRESS: PRESS_PATT,
|
|
||||||
LUX: LUX_PATT
|
|
||||||
}
|
|
||||||
UNITS = {
|
|
||||||
TEMP: TEMP_CELSIUS,
|
|
||||||
HUM: PERCENTAGE,
|
|
||||||
PRESS: 'mmHg',
|
|
||||||
LUX: LIGHT_LUX
|
|
||||||
}
|
|
||||||
CLASSES = {
|
CLASSES = {
|
||||||
TEMP: DEVICE_CLASS_TEMPERATURE,
|
TEMP: SensorDeviceClass.TEMPERATURE,
|
||||||
HUM: DEVICE_CLASS_HUMIDITY,
|
HUM: SensorDeviceClass.HUMIDITY,
|
||||||
PRESS: DEVICE_CLASS_PRESSURE,
|
PRESS: SensorDeviceClass.PRESSURE,
|
||||||
LUX: DEVICE_CLASS_ILLUMINANCE
|
LUX: SensorDeviceClass.ILLUMINANCE,
|
||||||
}
|
}
|
||||||
I2C_DEVICE_TYPES = {
|
I2C_DEVICE_TYPES = {
|
||||||
"2": LUX, # BH1750
|
"2": LUX, # BH1750
|
||||||
"3": LUX, # TSL2591
|
"3": LUX, # TSL2591
|
||||||
"7": LUX, # MAX44009
|
"7": LUX, # MAX44009
|
||||||
"70": LUX, # OPT3001
|
"70": LUX, # OPT3001
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,46 +61,48 @@ class MegaD:
|
|||||||
"""MegaD Hub"""
|
"""MegaD Hub"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
loop: asyncio.AbstractEventLoop,
|
loop: asyncio.AbstractEventLoop,
|
||||||
host: str,
|
host: str,
|
||||||
password: str,
|
password: str,
|
||||||
mqtt: mqtt.MQTT,
|
lg: logging.Logger,
|
||||||
lg: logging.Logger,
|
id: str,
|
||||||
id: str,
|
config: ConfigEntry = None,
|
||||||
mqtt_inputs: bool = True,
|
mqtt_id: str = None,
|
||||||
mqtt_id: str = None,
|
scan_interval=60,
|
||||||
scan_interval=60,
|
port_to_scan=0,
|
||||||
port_to_scan=0,
|
nports=38,
|
||||||
nports=38,
|
update_all: bool = True,
|
||||||
update_all: bool=True,
|
poll_outs: bool = False,
|
||||||
poll_outs: bool=False,
|
fake_response: bool = True,
|
||||||
fake_response: bool=True,
|
force_d: bool = None,
|
||||||
force_d: bool=None,
|
allow_hosts: str = None,
|
||||||
allow_hosts: str=None,
|
protected=True,
|
||||||
protected=True,
|
restore_on_restart=False,
|
||||||
restore_on_restart=False,
|
extenders=None,
|
||||||
extenders=None,
|
ext_in=None,
|
||||||
ext_in=None,
|
ext_acts=None,
|
||||||
ext_acts=None,
|
i2c_sensors=None,
|
||||||
i2c_sensors=None,
|
new_naming=False,
|
||||||
new_naming=False,
|
update_time=False,
|
||||||
update_time=False,
|
smooth: list = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
|
self.skip_ports = set()
|
||||||
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
|
if config is not None:
|
||||||
if not self.http is None:
|
lg.debug(f"load config: %s", config.data)
|
||||||
self.http.allowed_hosts |= {host}
|
self.config = config
|
||||||
self.http.hubs[host] = self
|
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
|
||||||
if len(self.http.hubs) == 1:
|
if not self.http is None:
|
||||||
self.http.hubs['__def'] = self
|
self.http.allowed_hosts |= {host}
|
||||||
if mqtt_id:
|
self.http.hubs[host] = self
|
||||||
self.http.hubs[mqtt_id] = self
|
if len(self.http.hubs) == 1:
|
||||||
else:
|
self.http.hubs["__def"] = self
|
||||||
self.http = None
|
if mqtt_id:
|
||||||
|
self.http.hubs[mqtt_id] = self
|
||||||
|
self.smooth = smooth or []
|
||||||
self.new_naming = new_naming
|
self.new_naming = new_naming
|
||||||
self.extenders = extenders or []
|
self.extenders = extenders or []
|
||||||
self.ext_in = ext_in or {}
|
self.ext_in = ext_in or {}
|
||||||
@@ -111,12 +113,10 @@ class MegaD:
|
|||||||
self.update_all = update_all if update_all is not None else True
|
self.update_all = update_all if update_all is not None else True
|
||||||
self.nports = nports
|
self.nports = nports
|
||||||
self.fake_response = fake_response
|
self.fake_response = fake_response
|
||||||
self.mqtt_inputs = mqtt_inputs
|
|
||||||
self.loop: asyncio.AbstractEventLoop = None
|
self.loop: asyncio.AbstractEventLoop = None
|
||||||
self.hass = hass
|
self.hass = hass
|
||||||
self.host = host
|
self.host = host
|
||||||
self.sec = password
|
self.sec = password
|
||||||
self.mqtt = mqtt
|
|
||||||
self.id = id
|
self.id = id
|
||||||
self.lck = asyncio.Lock()
|
self.lck = asyncio.Lock()
|
||||||
self.last_long = {}
|
self.last_long = {}
|
||||||
@@ -133,7 +133,9 @@ class MegaD:
|
|||||||
self.sensors = []
|
self.sensors = []
|
||||||
self.port_to_scan = port_to_scan
|
self.port_to_scan = port_to_scan
|
||||||
self.last_update = datetime.now()
|
self.last_update = datetime.now()
|
||||||
self._callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] = defaultdict(list)
|
self._callbacks: typing.DefaultDict[
|
||||||
|
int, typing.List[typing.Callable[[dict], typing.Coroutine]]
|
||||||
|
] = defaultdict(list)
|
||||||
self._loop = loop
|
self._loop = loop
|
||||||
self._customize = None
|
self._customize = None
|
||||||
self.values = {}
|
self.values = {}
|
||||||
@@ -143,10 +145,12 @@ class MegaD:
|
|||||||
self.lg,
|
self.lg,
|
||||||
name="megad",
|
name="megad",
|
||||||
update_method=self.poll,
|
update_method=self.poll,
|
||||||
update_interval=timedelta(seconds=self.poll_interval) if self.poll_interval else None,
|
update_interval=timedelta(seconds=self.poll_interval)
|
||||||
|
if self.poll_interval
|
||||||
|
else None,
|
||||||
)
|
)
|
||||||
self.updaters = []
|
self.updaters = []
|
||||||
self.fw = ''
|
self.fw = ""
|
||||||
self.notifiers = defaultdict(asyncio.Condition)
|
self.notifiers = defaultdict(asyncio.Condition)
|
||||||
if not mqtt_id:
|
if not mqtt_id:
|
||||||
_id = host.split(".")[-1]
|
_id = host.split(".")[-1]
|
||||||
@@ -157,23 +161,16 @@ class MegaD:
|
|||||||
if force_d is not None:
|
if force_d is not None:
|
||||||
self.customize[CONF_FORCE_D] = force_d
|
self.customize[CONF_FORCE_D] = force_d
|
||||||
try:
|
try:
|
||||||
if allow_hosts is not None:
|
if allow_hosts is not None and DOMAIN in hass.data:
|
||||||
allow_hosts = set(allow_hosts.split(';'))
|
allow_hosts = set(allow_hosts.split(";"))
|
||||||
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
|
hass.data[DOMAIN][CONF_HTTP].allowed_hosts |= allow_hosts
|
||||||
hass.data[DOMAIN][CONF_HTTP].protected = protected
|
hass.data[DOMAIN][CONF_HTTP].protected = protected
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
self.lg.exception('while setting allowed hosts')
|
self.lg.exception("while setting allowed hosts")
|
||||||
|
self.binary_sensors = []
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
self.loop = asyncio.get_event_loop()
|
pass
|
||||||
if self.mqtt is not None:
|
|
||||||
set_events_off()
|
|
||||||
self.subs = await self.mqtt.async_subscribe(
|
|
||||||
topic=f"{self.mqtt_id}/+",
|
|
||||||
msg_callback=self._process_msg,
|
|
||||||
qos=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def stop(self):
|
async def stop(self):
|
||||||
if self.subs is not None:
|
if self.subs is not None:
|
||||||
@@ -189,11 +186,14 @@ class MegaD:
|
|||||||
self.lg.debug(self.sensors)
|
self.lg.debug(self.sensors)
|
||||||
ports = []
|
ports = []
|
||||||
for x in self.sensors:
|
for x in self.sensors:
|
||||||
if only_list and x.http_cmd != 'list':
|
if only_list and x.http_cmd != "list":
|
||||||
continue
|
continue
|
||||||
if x.port in ports:
|
if x.port in ports:
|
||||||
continue
|
continue
|
||||||
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
|
try:
|
||||||
|
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
continue
|
||||||
ports.append(x.port)
|
ports.append(x.port)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -214,22 +214,24 @@ class MegaD:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_online(self):
|
def is_online(self):
|
||||||
return (datetime.now() - self.last_update).total_seconds() < (self.poll_interval + 10)
|
return (datetime.now() - self.last_update).total_seconds() < (
|
||||||
|
self.poll_interval + 10
|
||||||
|
)
|
||||||
|
|
||||||
def _warn_offline(self):
|
def _warn_offline(self):
|
||||||
if self.online:
|
if self.online:
|
||||||
self.lg.warning('mega is offline')
|
self.lg.warning("mega is offline")
|
||||||
self.hass.states.async_set(
|
self.hass.states.async_set(
|
||||||
f'mega.{self.id}',
|
f"mega.{self.id}",
|
||||||
'offline',
|
"offline",
|
||||||
)
|
)
|
||||||
self.online = False
|
self.online = False
|
||||||
|
|
||||||
def _notify_online(self):
|
def _notify_online(self):
|
||||||
if not self.online:
|
if not self.online:
|
||||||
self.hass.states.async_set(
|
self.hass.states.async_set(
|
||||||
f'mega.{self.id}',
|
f"mega.{self.id}",
|
||||||
'online',
|
"online",
|
||||||
)
|
)
|
||||||
self.online = True
|
self.online = True
|
||||||
|
|
||||||
@@ -239,19 +241,19 @@ class MegaD:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for x in self.ds2413_ports:
|
for x in self.ds2413_ports:
|
||||||
self.lg.debug(f'poll ds2413 for %s', x)
|
self.lg.debug(f"poll ds2413 for %s", x)
|
||||||
await self.get_port(
|
try:
|
||||||
port=x,
|
await self.get_port(
|
||||||
force_http=True,
|
port=x, force_http=True, http_cmd="list", conv=False
|
||||||
http_cmd='list',
|
)
|
||||||
conv=False
|
except asyncio.TimeoutError:
|
||||||
)
|
continue
|
||||||
|
|
||||||
async def poll(self):
|
async def poll(self):
|
||||||
"""
|
"""
|
||||||
Polling ports
|
Polling ports
|
||||||
"""
|
"""
|
||||||
self.lg.debug('poll')
|
self.lg.debug("poll")
|
||||||
if self._update_time:
|
if self._update_time:
|
||||||
await self.update_time()
|
await self.update_time()
|
||||||
for x in self.i2c_sensors:
|
for x in self.i2c_sensors:
|
||||||
@@ -260,34 +262,29 @@ class MegaD:
|
|||||||
ret = await self._update_i2c(x)
|
ret = await self._update_i2c(x)
|
||||||
if isinstance(ret, dict):
|
if isinstance(ret, dict):
|
||||||
self.values.update(ret)
|
self.values.update(ret)
|
||||||
|
|
||||||
for x in self.extenders:
|
for x in self.extenders:
|
||||||
ret = await self._update_extender(x)
|
ret = await self._update_extender(x)
|
||||||
if not isinstance(ret, dict):
|
if not isinstance(ret, dict):
|
||||||
self.lg.warning(f'wrong updater result: {ret} from extender {x}')
|
self.lg.warning(f"wrong updater result: {ret} from extender {x}")
|
||||||
continue
|
continue
|
||||||
self.values.update(ret)
|
self.values.update(ret)
|
||||||
if self.mqtt is None:
|
|
||||||
await self.get_all_ports()
|
await self.get_all_ports()
|
||||||
await self.get_sensors(only_list=True)
|
await self.get_sensors(only_list=True)
|
||||||
elif self.poll_outs:
|
|
||||||
await self.get_all_ports(check_skip=True)
|
|
||||||
elif len(self.sensors) > 0:
|
|
||||||
await self.get_sensors()
|
|
||||||
# else:
|
|
||||||
# await self.get_port(self.port_to_scan)
|
|
||||||
await self._get_ds2413()
|
await self._get_ds2413()
|
||||||
return self.values
|
return self.values
|
||||||
|
|
||||||
async def get_mqtt_id(self):
|
async def get_mqtt_id(self):
|
||||||
async with aiohttp.request(
|
async with aiohttp.request(
|
||||||
'get', f'http://{self.host}/{self.sec}/?cf=2'
|
"get", f"http://{self.host}/{self.sec}/?cf=2"
|
||||||
) as req:
|
) as req:
|
||||||
data = await req.text()
|
data = await req.text(encoding="iso-8859-5")
|
||||||
data = BeautifulSoup(data, features="lxml")
|
data = BeautifulSoup(data, features="lxml")
|
||||||
_id = data.find(attrs={'name': 'mdid'})
|
_id = data.find(attrs={"name": "mdid"})
|
||||||
if _id:
|
if _id:
|
||||||
_id = _id['value']
|
_id = _id["value"]
|
||||||
return _id or 'megad/' + self.host.split('.')[-1]
|
return _id or "megad/" + self.host.split(".")[-1]
|
||||||
|
|
||||||
async def get_fw(self):
|
async def get_fw(self):
|
||||||
data = await self.request()
|
data = await self.request()
|
||||||
@@ -297,102 +294,96 @@ class MegaD:
|
|||||||
return await self.request(pt=port, cmd=cmd)
|
return await self.request(pt=port, cmd=cmd)
|
||||||
|
|
||||||
async def request(self, priority=0, **kwargs):
|
async def request(self, priority=0, **kwargs):
|
||||||
cmd = '&'.join([f'{k}={v}' for k, v in kwargs.items() if v is not None])
|
cmd = "&".join([f"{k}={v}" for k, v in kwargs.items() if v is not None])
|
||||||
url = f"http://{self.host}/{self.sec}"
|
url = f"http://{self.host}/{self.sec}"
|
||||||
if cmd:
|
if cmd:
|
||||||
url = f"{url}/?{cmd}"
|
url = f"{url}/?{cmd}"
|
||||||
self.lg.debug('request: %s', url)
|
self.lg.debug("request: %s", url)
|
||||||
async with self._http_lck(priority):
|
async with self._http_lck(priority):
|
||||||
async with aiohttp.request("get", url=url) as req:
|
for _ntry in range(3):
|
||||||
if req.status != 200:
|
try:
|
||||||
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
|
async with aiohttp.request(
|
||||||
return None
|
"get", url=url, timeout=aiohttp.ClientTimeout(total=5)
|
||||||
else:
|
) as req:
|
||||||
ret = await req.text()
|
if req.status != 200:
|
||||||
self.lg.debug('response %s', ret)
|
self.lg.warning(
|
||||||
return ret
|
"%s returned %s (%s)",
|
||||||
|
url,
|
||||||
|
req.status,
|
||||||
|
await req.text(encoding="iso-8859-5"),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
ret = await req.text(encoding="iso-8859-5")
|
||||||
|
self.lg.debug("response %s", ret)
|
||||||
|
return ret
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
self.lg.warning(f"timeout while requesting {url}")
|
||||||
|
# raise
|
||||||
|
await asyncio.sleep(1)
|
||||||
|
raise asyncio.TimeoutError("after 3 tries")
|
||||||
|
|
||||||
async def save(self):
|
async def save(self):
|
||||||
await self.send_command(cmd='s')
|
await self.send_command(cmd="s")
|
||||||
|
|
||||||
def parse_response(self, ret, cmd='get'):
|
def parse_response(self, ret, cmd="get"):
|
||||||
if ret is None:
|
if ret is None:
|
||||||
raise NoPort()
|
raise NoPort()
|
||||||
if 'busy' in ret:
|
if "busy" in ret:
|
||||||
return None
|
return None
|
||||||
if ':' in ret:
|
if ":" in ret:
|
||||||
if ';' in ret:
|
if ";" in ret:
|
||||||
ret = ret.split(';')
|
ret = ret.split(";")
|
||||||
elif '/' in ret and not cmd == 'list':
|
elif "/" in ret and not cmd == "list":
|
||||||
ret = ret.split('/')
|
ret = ret.split("/")
|
||||||
else:
|
else:
|
||||||
ret = [ret]
|
ret = [ret]
|
||||||
ret = {'value': dict([
|
ret = {"value": dict([x.split(":") for x in ret if x.count(":") == 1])}
|
||||||
x.split(':') for x in ret if x.count(':') == 1
|
elif "ON" in ret:
|
||||||
])}
|
ret = {"value": "ON"}
|
||||||
elif 'ON' in ret:
|
elif "OFF" in ret:
|
||||||
ret = {'value': 'ON'}
|
ret = {"value": "OFF"}
|
||||||
elif 'OFF' in ret:
|
|
||||||
ret = {'value': 'OFF'}
|
|
||||||
else:
|
else:
|
||||||
ret = {'value': ret}
|
ret = {"value": ret}
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def get_port(self, port, force_http=False, http_cmd='get', conv=True):
|
async def get_port(self, port, force_http=False, http_cmd="get", conv=True):
|
||||||
"""
|
"""
|
||||||
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
|
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
|
||||||
хранилище values
|
хранилище values
|
||||||
"""
|
"""
|
||||||
self.lg.debug(f'get port %s', port)
|
self.lg.debug(f"get port %s", port)
|
||||||
if self.mqtt is None or force_http:
|
if http_cmd == "list" and conv:
|
||||||
if http_cmd == 'list' and conv:
|
await self.request(pt=port, cmd="conv")
|
||||||
await self.request(pt=port, cmd='conv')
|
await asyncio.sleep(1)
|
||||||
await asyncio.sleep(1)
|
ret = self.parse_response(
|
||||||
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd)
|
await self.request(pt=port, cmd=http_cmd), cmd=http_cmd
|
||||||
ntry = 0
|
)
|
||||||
while http_cmd == 'list' and ret is None and ntry < 3:
|
ntry = 0
|
||||||
await asyncio.sleep(1)
|
while http_cmd == "list" and ret is None and ntry < 3:
|
||||||
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
|
await asyncio.sleep(1)
|
||||||
ntry += 1
|
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
|
||||||
self.lg.debug('parsed: %s', ret)
|
ntry += 1
|
||||||
self.values[port] = ret
|
self.lg.debug("parsed: %s", ret)
|
||||||
return ret
|
self.values[port] = ret
|
||||||
|
return ret
|
||||||
async with self._notif_lck:
|
|
||||||
async with self.notifiers[port]:
|
|
||||||
cnd = self.notifiers[port]
|
|
||||||
await self.mqtt.async_publish(
|
|
||||||
topic=f'{self.mqtt_id}/cmd',
|
|
||||||
payload=f'get:{port}',
|
|
||||||
qos=2,
|
|
||||||
retain=False,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await asyncio.wait_for(cnd.wait(), timeout=10)
|
|
||||||
return self.values.get(port)
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
self.lg.error(f'timeout when getting port {port}')
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ports(self):
|
def ports(self):
|
||||||
return {e.port for e in self.entities}
|
return {e.port for e in self.entities}
|
||||||
|
|
||||||
async def get_all_ports(self, only_out=False, check_skip=False):
|
async def get_all_ports(self, only_out=False, check_skip=False):
|
||||||
if not self.mqtt_inputs:
|
try:
|
||||||
ret = await self.request(cmd='all')
|
ret = await self.request(cmd="all")
|
||||||
for port, x in enumerate(ret.split(';')):
|
except asyncio.TimeoutError:
|
||||||
if port in self.ds2413_ports:
|
return
|
||||||
continue
|
for port, x in enumerate(ret.split(";")):
|
||||||
if check_skip and not port in self.ports:
|
if port in self.ds2413_ports:
|
||||||
continue
|
continue
|
||||||
ret = self.parse_response(x)
|
if check_skip and not port in self.ports:
|
||||||
self.values[port] = ret
|
continue
|
||||||
elif not check_skip:
|
ret = self.parse_response(x)
|
||||||
for x in range(self.nports + 1):
|
self.values[port] = ret
|
||||||
await self.get_port(x)
|
|
||||||
else:
|
|
||||||
for x in self.ports:
|
|
||||||
await self.get_port(x)
|
|
||||||
|
|
||||||
async def reboot(self, save=True):
|
async def reboot(self, save=True):
|
||||||
await self.save()
|
await self.save()
|
||||||
@@ -404,22 +395,20 @@ class MegaD:
|
|||||||
|
|
||||||
def _process_msg(self, msg):
|
def _process_msg(self, msg):
|
||||||
try:
|
try:
|
||||||
d = msg.topic.split('/')
|
d = msg.topic.split("/")
|
||||||
port = d[-1]
|
port = d[-1]
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.lg.warning('can not process %s', msg)
|
self.lg.warning("can not process %s", msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
if port == 'cmd':
|
if port == "cmd":
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
port = int_ignore(port)
|
port = int_ignore(port)
|
||||||
except:
|
except:
|
||||||
self.lg.warning('can not process %s', msg)
|
self.lg.warning("can not process %s", msg)
|
||||||
return
|
return
|
||||||
self.lg.debug(
|
self.lg.debug("process incomming message: %s", msg)
|
||||||
'process incomming message: %s', msg
|
|
||||||
)
|
|
||||||
value = None
|
value = None
|
||||||
try:
|
try:
|
||||||
value = json.loads(msg.payload)
|
value = json.loads(msg.payload)
|
||||||
@@ -430,13 +419,13 @@ class MegaD:
|
|||||||
cb(value)
|
cb(value)
|
||||||
if isinstance(value, dict):
|
if isinstance(value, dict):
|
||||||
value = value.copy()
|
value = value.copy()
|
||||||
value['mega_id'] = self.id
|
value["mega_id"] = self.id
|
||||||
self.hass.bus.async_fire(
|
self.hass.bus.async_fire(
|
||||||
EVENT_BINARY_SENSOR,
|
EVENT_BINARY_SENSOR,
|
||||||
value,
|
value,
|
||||||
)
|
)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
|
self.lg.warning(f"could not parse json ({msg.payload}): {exc}")
|
||||||
return
|
return
|
||||||
finally:
|
finally:
|
||||||
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
|
asyncio.run_coroutine_threadsafe(self._notify(port, value), self.loop)
|
||||||
@@ -444,17 +433,15 @@ 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",
|
||||||
|
port,
|
||||||
)
|
)
|
||||||
if self.mqtt_inputs:
|
self.http.callbacks[self.id][port].append(callback)
|
||||||
self._callbacks[port].append(callback)
|
|
||||||
else:
|
|
||||||
self.http.callbacks[self.id][port].append(callback)
|
|
||||||
|
|
||||||
async def authenticate(self) -> bool:
|
async def authenticate(self) -> bool:
|
||||||
"""Test if we can authenticate with the host."""
|
"""Test if we can authenticate with the host."""
|
||||||
async with aiohttp.request("get", url=f"http://{self.host}/{self.sec}") as req:
|
async with aiohttp.request("get", url=f"http://{self.host}/{self.sec}") as req:
|
||||||
if "Unauthorized" in await req.text():
|
if "Unauthorized" in await req.text(encoding="iso-8859-5"):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if req.status != 200:
|
if req.status != 200:
|
||||||
@@ -462,60 +449,21 @@ class MegaD:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_port_page(self, port):
|
async def get_port_page(self, port):
|
||||||
url = f'http://{self.host}/{self.sec}/?pt={port}'
|
url = f"http://{self.host}/{self.sec}/?pt={port}"
|
||||||
self.lg.debug(f'get page for port {port} {url}')
|
self.lg.debug(f"get page for port {port} {url}")
|
||||||
async with aiohttp.request('get', url) as req:
|
async with aiohttp.request("get", url) as req:
|
||||||
return await req.text()
|
return await req.text(encoding="iso-8859-5")
|
||||||
|
|
||||||
async def scan_port(self, port):
|
async def scan_port(self, port):
|
||||||
data = await self.request(pt=port)
|
data = await self.request(pt=port)
|
||||||
return parse_config(data)
|
return parse_config(data)
|
||||||
# async with self.lck:
|
|
||||||
# if port in self._scanned:
|
|
||||||
# return self._scanned[port]
|
|
||||||
# url = f'http://{self.host}/{self.sec}/?pt={port}'
|
|
||||||
# self.lg.debug(
|
|
||||||
# f'scan port %s: %s', port, url
|
|
||||||
# )
|
|
||||||
# async with aiohttp.request('get', url) as req:
|
|
||||||
# html = await req.text()
|
|
||||||
# if req.status != 200:
|
|
||||||
# return
|
|
||||||
# tree = BeautifulSoup(html, features="lxml")
|
|
||||||
# pty = tree.find('select', attrs={'name': 'pty'})
|
|
||||||
# if pty is None:
|
|
||||||
# return
|
|
||||||
# else:
|
|
||||||
# pty = pty.find(selected=True)
|
|
||||||
# if pty:
|
|
||||||
# pty = pty['value']
|
|
||||||
# else:
|
|
||||||
# return
|
|
||||||
# if pty in ['0', '1']:
|
|
||||||
# m = tree.find('select', attrs={'name': 'm'})
|
|
||||||
# if m:
|
|
||||||
# m = m.find(selected=True)['value']
|
|
||||||
# self._scanned[port] = (pty, m)
|
|
||||||
# return pty, m
|
|
||||||
# elif pty == '3':
|
|
||||||
# m = tree.find('select', attrs={'name': 'd'})
|
|
||||||
# if m:
|
|
||||||
# m = m.find(selected=True)['value']
|
|
||||||
# self._scanned[port] = (pty, m)
|
|
||||||
# return pty, m
|
|
||||||
# elif pty in ('2', '4'): # эта часть не очень проработана, тут есть i2c который может работать неправильно
|
|
||||||
# m = tree.find('select', attrs={'name': 'd'})
|
|
||||||
# if m:
|
|
||||||
# m = m.find(selected=True)['value']
|
|
||||||
# self._scanned[port] = (pty, m or '0')
|
|
||||||
# return pty, m or '0'
|
|
||||||
|
|
||||||
async def scan_ports(self, nports=37):
|
async def scan_ports(self, nports=37):
|
||||||
for x in range(0, nports+1):
|
for x in range(0, nports + 1):
|
||||||
ret = await self.scan_port(x)
|
ret = await self.scan_port(x)
|
||||||
if ret:
|
if ret:
|
||||||
yield x, ret
|
yield x, ret
|
||||||
self.nports = nports+1
|
self.nports = nports + 1
|
||||||
|
|
||||||
async def _update_extender(self, port):
|
async def _update_extender(self, port):
|
||||||
"""
|
"""
|
||||||
@@ -523,10 +471,13 @@ class MegaD:
|
|||||||
:param port:
|
:param port:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
values = await self.request(pt=port, cmd='get')
|
try:
|
||||||
|
values = await self.request(pt=port, cmd="get")
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return
|
||||||
ret = {}
|
ret = {}
|
||||||
for i, x in enumerate(values.split(';')):
|
for i, x in enumerate(values.split(";")):
|
||||||
ret[f'{port}e{i}'] = x
|
ret[f"{port}e{i}"] = x
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def _update_i2c(self, params):
|
async def _update_i2c(self, params):
|
||||||
@@ -535,114 +486,161 @@ class MegaD:
|
|||||||
:param params: параметры url
|
:param params: параметры url
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
|
pt = params.get("pt")
|
||||||
|
if pt in self.skip_ports:
|
||||||
|
return
|
||||||
|
if pt is not None:
|
||||||
|
pass
|
||||||
_params = tuple(params.items())
|
_params = tuple(params.items())
|
||||||
delay = None
|
delay = None
|
||||||
if 'delay' in params:
|
if "delay" in params:
|
||||||
delay = params.pop('delay')
|
delay = params.pop("delay")
|
||||||
ret = {
|
try:
|
||||||
_params: await self.request(**params)
|
ret = {_params: await self.request(**params)}
|
||||||
}
|
except asyncio.TimeoutError:
|
||||||
self.lg.debug('i2c response: %s', ret)
|
return
|
||||||
|
self.lg.debug("i2c response: %s", ret)
|
||||||
if delay:
|
if delay:
|
||||||
self.lg.debug('delay %s', delay)
|
self.lg.debug("delay %s", delay)
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def get_config(self, nports=37):
|
async def get_config(self, nports=37):
|
||||||
ret = defaultdict(lambda: defaultdict(list))
|
ret = defaultdict(lambda: defaultdict(list))
|
||||||
ret['mqtt_id'] = await self.get_mqtt_id()
|
ret["mqtt_id"] = await self.get_mqtt_id()
|
||||||
ret['extenders'] = extenders = []
|
ret["extenders"] = extenders = []
|
||||||
ret['ext_in'] = ext_int = {}
|
ret["ext_in"] = ext_int = {}
|
||||||
ret['ext_acts'] = ext_acts = {}
|
ret["ext_acts"] = ext_acts = {}
|
||||||
ret['i2c_sensors'] = i2c_sensors = []
|
ret["i2c_sensors"] = i2c_sensors = []
|
||||||
|
ret["smooth"] = smooth = []
|
||||||
async for port, cfg in self.scan_ports(nports):
|
async for port, cfg in self.scan_ports(nports):
|
||||||
_cust = self.customize.get(port)
|
_cust = self.customize.get(port)
|
||||||
if not isinstance(_cust, dict):
|
if not isinstance(_cust, dict):
|
||||||
_cust = {}
|
_cust = {}
|
||||||
if cfg.pty == "0":
|
if cfg.pty == "0":
|
||||||
ret['binary_sensor'][port].append({})
|
ret["binary_sensor"][port].append({})
|
||||||
elif cfg.pty == "1" and (cfg.m in ['0', '1', '3'] or cfg.m is None):
|
elif cfg.pty == "1" and (cfg.m in ["0", "1", "3"] or cfg.m is None):
|
||||||
ret['light'][port].append({'dimmer': cfg.m == '1'})
|
if cfg.misc is not None:
|
||||||
|
smooth.append(port)
|
||||||
|
ret["light"][port].append(
|
||||||
|
{"dimmer": cfg.m == "1", "smooth": safe_int(cfg.misc)}
|
||||||
|
)
|
||||||
elif cfg == DS2413:
|
elif cfg == DS2413:
|
||||||
# ds2413
|
# ds2413
|
||||||
_data = await self.get_port(port=port, force_http=True, http_cmd='list', conv=False)
|
_data = await self.get_port(
|
||||||
data = _data.get('value', {})
|
port=port, force_http=True, http_cmd="list", conv=False
|
||||||
|
)
|
||||||
|
data = _data.get("value", {})
|
||||||
if not isinstance(data, dict):
|
if not isinstance(data, dict):
|
||||||
self.lg.warning(f'can not add ds2413 on port {port}, it has wrong data: {_data}')
|
self.lg.warning(
|
||||||
|
f"can not add ds2413 on port {port}, it has wrong data: {_data}"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
for addr, state in data.items():
|
for addr, state in data.items():
|
||||||
ret['light'][port].extend([
|
ret["light"][port].extend(
|
||||||
{"index": 0, "addr": addr, "id_suffix": f'{addr}_a', "http_cmd": 'ds2413'},
|
[
|
||||||
{"index": 1, "addr": addr, "id_suffix": f'{addr}_b', "http_cmd": 'ds2413'},
|
{
|
||||||
])
|
"index": 0,
|
||||||
|
"addr": addr,
|
||||||
|
"id_suffix": f"{addr}_a",
|
||||||
|
"http_cmd": "ds2413",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"index": 1,
|
||||||
|
"addr": addr,
|
||||||
|
"id_suffix": f"{addr}_b",
|
||||||
|
"http_cmd": "ds2413",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
elif cfg == MCP230:
|
elif cfg == MCP230:
|
||||||
extenders.append(port)
|
extenders.append(port)
|
||||||
if cfg.inta:
|
if cfg.inta:
|
||||||
ext_int[int_ignore(cfg.inta)] = port
|
ext_int[int_ignore(cfg.inta)] = port
|
||||||
values = await self.request(pt=port, cmd='get')
|
values = await self.request(pt=port, cmd="get")
|
||||||
values = values.split(';')
|
values = values.split(";")
|
||||||
for n in range(len(values)):
|
for n in range(len(values)):
|
||||||
ext_page = await self.request(pt=port, ext=n)
|
ext_page = await self.request(pt=port, ext=n)
|
||||||
ext_cfg = parse_config(ext_page)
|
ext_cfg = parse_config(ext_page)
|
||||||
pt = f'{port}e{n}'
|
pt = f"{port}e{n}" if not self.new_naming else f"{port:02d}e{n:02d}"
|
||||||
if ext_cfg.ety == '1':
|
if ext_cfg.ety == "1":
|
||||||
ret['light'][pt].append({})
|
ret["light"][pt].append({})
|
||||||
elif ext_cfg.ety == '0':
|
elif ext_cfg.ety == "0":
|
||||||
if ext_cfg.eact:
|
if ext_cfg.eact:
|
||||||
ext_acts[pt] = ext_cfg.eact
|
ext_acts[pt] = ext_cfg.eact
|
||||||
ret['binary_sensor'][pt].append({})
|
ret["binary_sensor"][pt].append({})
|
||||||
elif cfg == PCA9685:
|
elif cfg == PCA9685:
|
||||||
extenders.append(port)
|
extenders.append(port)
|
||||||
values = await self.request(pt=port, cmd='get')
|
values = await self.request(pt=port, cmd="get")
|
||||||
values = values.split(';')
|
values = values.split(";")
|
||||||
for n in range(len(values)):
|
for n in range(len(values)):
|
||||||
ret['light'][f'{port}e{n}'].append({'dimmer': True, 'dimmer_scale': 16})
|
pt = f"{port}e{n}"
|
||||||
elif cfg.pty == '4' and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)):
|
name = pt if not self.new_naming else f"{port:02}e{n:02}"
|
||||||
|
ret["light"][pt].append(
|
||||||
|
{
|
||||||
|
"dimmer": True,
|
||||||
|
"dimmer_scale": 16,
|
||||||
|
"name": f"{self.id}_{name}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if cfg.pty == "4": # and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN))
|
||||||
# i2c в режиме ANY
|
# i2c в режиме ANY
|
||||||
scan = cfg.src.find('a', text='I2C Scan')
|
scan = cfg.src.find("a", text="I2C Scan")
|
||||||
self.lg.debug(f'find scan link: %s', scan)
|
self.lg.debug(f"find scan link: %s", scan)
|
||||||
if scan is not None:
|
if scan is not None:
|
||||||
page = await self.request(pt=port, cmd='scan')
|
page = await self.request(pt=port, cmd="scan")
|
||||||
req, parsed = parse_scan_page(page)
|
req, parsed = parse_scan_page(page)
|
||||||
self.lg.debug(f'scan results: %s', (req, parsed))
|
self.lg.debug(f"scan results: %s", (req, parsed))
|
||||||
ret['i2c'][port].extend(parsed)
|
ret["i2c"][port].extend(parsed)
|
||||||
i2c_sensors.extend(req)
|
i2c_sensors.extend(req)
|
||||||
elif cfg.pty == '4' and cfg.m == '2':
|
elif cfg.pty == "4" and cfg.m == "2":
|
||||||
# scl исключаем из сканирования
|
# scl исключаем из сканирования
|
||||||
continue
|
continue
|
||||||
elif cfg.pty in ('3', '2', '4'):
|
elif cfg.pty is None and nports < 30:
|
||||||
http_cmd = 'get'
|
# вроде как это ADC на 328 меге
|
||||||
if cfg.d == '5' and cfg.pty == '3':
|
ret["sensor"][port].append(dict())
|
||||||
|
elif cfg.pty in ("3", "2", "4"):
|
||||||
|
http_cmd = "get"
|
||||||
|
if cfg.d == "5" and cfg.pty == "3":
|
||||||
# 1-wire bus
|
# 1-wire bus
|
||||||
values = await self.get_port(port, force_http=True, http_cmd='list')
|
values = await self.get_port(port, force_http=True, http_cmd="list")
|
||||||
http_cmd = 'list'
|
http_cmd = "list"
|
||||||
else:
|
else:
|
||||||
values = await self.get_port(port, force_http=True)
|
values = await self.get_port(port, force_http=True)
|
||||||
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
|
if values is None or (
|
||||||
values = await self.get_port(port, force_http=True, http_cmd='list')
|
isinstance(values, dict)
|
||||||
http_cmd = 'list'
|
and str(values.get("value")) in ("", "None")
|
||||||
self.lg.debug(f'values: %s', values)
|
):
|
||||||
|
values = await self.get_port(
|
||||||
|
port, force_http=True, http_cmd="list"
|
||||||
|
)
|
||||||
|
http_cmd = "list"
|
||||||
|
self.lg.debug(f"values: %s", values)
|
||||||
if values is None:
|
if values is None:
|
||||||
self.lg.warning(f'port {port} is of type sensor but response is None, skipping it')
|
self.lg.warning(
|
||||||
|
f"port {port} is of type sensor but response is None, skipping it"
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
if isinstance(values, dict) and 'value' in values:
|
if isinstance(values, dict) and "value" in values:
|
||||||
values = values['value']
|
values = values["value"]
|
||||||
if isinstance(values, str) and TEMP_PATT.search(values):
|
if isinstance(values, str) and TEMP_PATT.search(values):
|
||||||
values = {TEMP: values}
|
values = {TEMP: values}
|
||||||
elif not isinstance(values, dict):
|
elif not isinstance(values, dict):
|
||||||
if cfg.pty == '4' and cfg.d in I2C_DEVICE_TYPES:
|
if cfg.pty == "4" and cfg.d in I2C_DEVICE_TYPES:
|
||||||
values = {I2C_DEVICE_TYPES.get(cfg.m): values}
|
values = {I2C_DEVICE_TYPES.get(cfg.m): values}
|
||||||
else:
|
else:
|
||||||
values = {None: values}
|
values = {None: values}
|
||||||
for key in values:
|
for key in values:
|
||||||
self.lg.debug(f'add sensor {key}')
|
self.lg.debug(f"add sensor {key}")
|
||||||
ret['sensor'][port].append(dict(
|
ret["sensor"][port].append(
|
||||||
key=key,
|
dict(
|
||||||
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
|
key=key,
|
||||||
device_class=CLASSES.get(key, CLASSES[TEMP]),
|
unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
|
||||||
id_suffix=key,
|
device_class=CLASSES.get(key, CLASSES[TEMP]),
|
||||||
http_cmd=http_cmd,
|
id_suffix=key,
|
||||||
))
|
http_cmd=http_cmd,
|
||||||
|
)
|
||||||
|
)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
async def restore_states(self):
|
async def restore_states(self):
|
||||||
@@ -654,7 +652,102 @@ class MegaD:
|
|||||||
await x.async_turn_off()
|
await x.async_turn_off()
|
||||||
|
|
||||||
async def update_time(self):
|
async def update_time(self):
|
||||||
await self.request(
|
await self.request(cf=7, stime=datetime.now().strftime("%H:%M:%S"))
|
||||||
cf=7,
|
|
||||||
stime=datetime.now().strftime('%H:%M:%S')
|
async def reload(self, reload_entry=True):
|
||||||
)
|
new = await self.get_config(nports=self.nports)
|
||||||
|
cfg = dict(self.config.data)
|
||||||
|
for x in REMOVE_CONFIG:
|
||||||
|
cfg.pop(x, None)
|
||||||
|
cfg.update(new)
|
||||||
|
self.lg.debug(f"new config: %s", cfg)
|
||||||
|
self.config.data = cfg
|
||||||
|
if reload_entry:
|
||||||
|
await self.hass.config_entries.async_reload(self.config.entry_id)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def _wrap_port_smooth(self, from_, to_, time):
|
||||||
|
self.lg.debug("dim from %s to %s for %s seconds", from_, to_, time)
|
||||||
|
if time <= 0:
|
||||||
|
return
|
||||||
|
beg = datetime.now()
|
||||||
|
diff = to_ - from_
|
||||||
|
while True:
|
||||||
|
_pct = (datetime.now() - beg).total_seconds() / time
|
||||||
|
if _pct > 1:
|
||||||
|
return
|
||||||
|
val = from_ + round(diff * _pct)
|
||||||
|
yield val
|
||||||
|
|
||||||
|
async def smooth_dim(
|
||||||
|
self,
|
||||||
|
*config: typing.Tuple[typing.Any, int, int],
|
||||||
|
time: float,
|
||||||
|
jitter: int = 50,
|
||||||
|
ws=False,
|
||||||
|
updater=None,
|
||||||
|
can_smooth_hardware=False,
|
||||||
|
max_values=None,
|
||||||
|
chip=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Плавное диммирование силами сервера, сразу нескольких портов (одной командой)
|
||||||
|
|
||||||
|
:param config: [(port, from, to), (port, from, to)]
|
||||||
|
:param time: время на диммирование
|
||||||
|
:param jitter: дополнительное замедление между командами в милисекундах
|
||||||
|
:param ws: если True, используется режим ws21xx
|
||||||
|
:param updater: функция, в которую передается текущее состояние
|
||||||
|
:param can_smooth_hardware: если True, используется аппаратная реализация smooth
|
||||||
|
:param max_values: максимальные значения (необходимы для расчета тайминга аппаратного smooth)
|
||||||
|
:param chip: кол-во чипов для ws-лент
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if can_smooth_hardware:
|
||||||
|
for i, (pt, from_, to_) in enumerate(config):
|
||||||
|
pct = abs(from_ - to_) / max_values[i]
|
||||||
|
tm = max([round(time / pct), 1])
|
||||||
|
await self.request(pt=pt, pwm=to_, cnt=tm)
|
||||||
|
|
||||||
|
last_step = tuple([to_ for (_, _, to_) in config])
|
||||||
|
gen = [self._wrap_port_smooth(f, t, time) for (_, f, t) in config]
|
||||||
|
c = None
|
||||||
|
stop = False
|
||||||
|
while True:
|
||||||
|
if stop:
|
||||||
|
return
|
||||||
|
await asyncio.sleep(jitter / 1000)
|
||||||
|
try:
|
||||||
|
_next_val = tuple([next(x) for x in gen])
|
||||||
|
except StopIteration:
|
||||||
|
_next_val = last_step
|
||||||
|
stop = True
|
||||||
|
if _next_val == c:
|
||||||
|
continue
|
||||||
|
if updater is not None:
|
||||||
|
updater(_next_val)
|
||||||
|
if can_smooth_hardware:
|
||||||
|
if _next_val == last_step:
|
||||||
|
return
|
||||||
|
continue
|
||||||
|
if not ws:
|
||||||
|
cmd = dict(
|
||||||
|
cmd=";".join(
|
||||||
|
[f"{pt}:{_next_val[i]}" for i, (pt, _, _) in enumerate(config)]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await self.request(**cmd)
|
||||||
|
else:
|
||||||
|
# для адресных лент
|
||||||
|
cmd = dict(
|
||||||
|
pt=config[0][0],
|
||||||
|
chip=chip,
|
||||||
|
ws="".join(
|
||||||
|
[hex(x).split("x")[1].rjust(2, "0").upper() for x in _next_val]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await self.request(**cmd)
|
||||||
|
|
||||||
|
if _next_val == last_step:
|
||||||
|
return
|
||||||
|
c = _next_val
|
||||||
|
|||||||
@@ -1,12 +1,27 @@
|
|||||||
from dataclasses import dataclass, field
|
import typing
|
||||||
|
from dataclasses import dataclass, astuple
|
||||||
from urllib.parse import parse_qsl, urlparse
|
from urllib.parse import parse_qsl, urlparse
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
from homeassistant.components.sensor import SensorDeviceClass
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
DEVICE_CLASS_HUMIDITY,
|
PERCENTAGE,
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
LIGHT_LUX,
|
||||||
DEVICE_CLASS_ILLUMINANCE,
|
TEMP_CELSIUS,
|
||||||
DEVICE_CLASS_PRESSURE,
|
CONCENTRATION_PARTS_PER_MILLION,
|
||||||
|
PRESSURE_BAR,
|
||||||
)
|
)
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
|
# DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DeviceType:
|
||||||
|
device_class: typing.Optional[str] = None
|
||||||
|
unit_of_measurement: typing.Optional[str] = None
|
||||||
|
suffix: typing.Optional[str] = None
|
||||||
|
delay: typing.Optional[float] = None
|
||||||
|
|
||||||
|
|
||||||
def parse_scan_page(page: str):
|
def parse_scan_page(page: str):
|
||||||
@@ -23,38 +38,40 @@ def parse_scan_page(page: str):
|
|||||||
continue
|
continue
|
||||||
classes = i2c_classes.get(dev, [])
|
classes = i2c_classes.get(dev, [])
|
||||||
for i, c in enumerate(classes):
|
for i, c in enumerate(classes):
|
||||||
|
_params = params.copy()
|
||||||
if c is Skip:
|
if c is Skip:
|
||||||
continue
|
continue
|
||||||
elif c is Request:
|
elif c is Request:
|
||||||
req.append(params)
|
req.append(_params)
|
||||||
continue
|
continue
|
||||||
elif isinstance(c, Request):
|
elif isinstance(c, Request):
|
||||||
if c.delay:
|
if c.delay:
|
||||||
params = params.copy()
|
_params['delay'] = c.delay
|
||||||
params['delay'] = c.delay
|
req.append(_params)
|
||||||
req.append(params)
|
|
||||||
continue
|
continue
|
||||||
elif isinstance(c, tuple):
|
elif isinstance(c, DeviceType):
|
||||||
suffix, c = c
|
c, m, suffix, delay = astuple(c)
|
||||||
elif isinstance(c, str):
|
if delay is not None:
|
||||||
suffix = c
|
_params['delay'] = delay
|
||||||
else:
|
else:
|
||||||
suffix = ''
|
continue
|
||||||
if 'addr' in params:
|
suffix = suffix or c
|
||||||
suffix += f"_{params['addr']}" if suffix else str(params['addr'])
|
if 'addr' in _params:
|
||||||
|
suffix += f"_{_params['addr']}" if suffix else str(_params['addr'])
|
||||||
if suffix:
|
if suffix:
|
||||||
_dev = f'{dev}_{suffix}'
|
_dev = f'{dev}_{suffix}'
|
||||||
else:
|
else:
|
||||||
_dev = dev
|
_dev = dev
|
||||||
params = params.copy()
|
|
||||||
if i > 0:
|
if i > 0:
|
||||||
params['i2c_par'] = i
|
_params['i2c_par'] = i
|
||||||
|
|
||||||
ret.append({
|
ret.append({
|
||||||
'id_suffix': _dev,
|
'id_suffix': _dev,
|
||||||
'device_class': c,
|
'device_class': c,
|
||||||
'params': params,
|
'params': _params,
|
||||||
|
'unit_of_measurement': m,
|
||||||
})
|
})
|
||||||
req.append(params)
|
req.append(_params)
|
||||||
return req, ret
|
return req, ret
|
||||||
|
|
||||||
|
|
||||||
@@ -69,50 +86,66 @@ class Request:
|
|||||||
|
|
||||||
i2c_classes = {
|
i2c_classes = {
|
||||||
'htu21d': [
|
'htu21d': [
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
],
|
],
|
||||||
'sht31': [
|
'sht31': [
|
||||||
DEVICE_CLASS_HUMIDITY,
|
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=1.5),
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
],
|
],
|
||||||
'max44009': [
|
'max44009': [
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
|
||||||
],
|
],
|
||||||
'bh1750': [
|
'bh1750': [
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
|
||||||
],
|
],
|
||||||
'tsl2591': [
|
'tsl2591': [
|
||||||
DEVICE_CLASS_ILLUMINANCE
|
DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
|
||||||
],
|
],
|
||||||
'bmp180': [
|
'bmp180': [
|
||||||
DEVICE_CLASS_PRESSURE,
|
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
],
|
],
|
||||||
'bmx280': [
|
'bmx280': [
|
||||||
DEVICE_CLASS_PRESSURE,
|
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
DEVICE_CLASS_HUMIDITY
|
DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None)
|
||||||
|
],
|
||||||
|
'dps368': [
|
||||||
|
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
|
||||||
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
],
|
],
|
||||||
'mlx90614': [
|
'mlx90614': [
|
||||||
Skip,
|
Skip,
|
||||||
('temp', DEVICE_CLASS_TEMPERATURE),
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'temp'),
|
||||||
('object', DEVICE_CLASS_TEMPERATURE),
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'object'),
|
||||||
],
|
],
|
||||||
'ptsensor': [
|
'ptsensor': [
|
||||||
Skip,
|
Skip,
|
||||||
Request(delay=1), # запрос на измерение
|
Request(delay=3), # запрос на измерение
|
||||||
DEVICE_CLASS_PRESSURE,
|
DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
],
|
],
|
||||||
'mcp9600': [
|
'mcp9600': [
|
||||||
DEVICE_CLASS_TEMPERATURE, # термопара
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара
|
||||||
DEVICE_CLASS_TEMPERATURE, # сенсор встроенный в микросхему
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
|
||||||
],
|
],
|
||||||
't67xx': [
|
't67xx': [
|
||||||
None # для co2 нет класса в HA
|
DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None)
|
||||||
],
|
],
|
||||||
'tmp117': [
|
'tmp117': [
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
|
||||||
]
|
],
|
||||||
|
'ads1115': [
|
||||||
|
DeviceType(None, None, 'ch0'),
|
||||||
|
DeviceType(None, None, 'ch1'),
|
||||||
|
DeviceType(None, None, 'ch2'),
|
||||||
|
DeviceType(None, None, 'ch3'),
|
||||||
|
],
|
||||||
|
'ads1015': [
|
||||||
|
DeviceType(None, None, 'ch0'),
|
||||||
|
DeviceType(None, None, 'ch1'),
|
||||||
|
DeviceType(None, None, 'ch2'),
|
||||||
|
DeviceType(None, None, 'ch3'),
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
"""Platform for light integration."""
|
"""Platform for light integration."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import typing
|
||||||
|
from datetime import timedelta, datetime
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
import colorsys
|
||||||
|
import time
|
||||||
|
|
||||||
from homeassistant.components.light import (
|
from homeassistant.components.light import (
|
||||||
PLATFORM_SCHEMA as LIGHT_SCHEMA,
|
PLATFORM_SCHEMA as LIGHT_SCHEMA,
|
||||||
SUPPORT_BRIGHTNESS,
|
SUPPORT_BRIGHTNESS,
|
||||||
LightEntity,
|
LightEntity,
|
||||||
|
SUPPORT_TRANSITION,
|
||||||
|
SUPPORT_COLOR,
|
||||||
|
ColorMode,
|
||||||
|
LightEntityFeature,
|
||||||
|
# SUPPORT_WHITE_VALUE
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@@ -16,7 +30,7 @@ from homeassistant.const import (
|
|||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from .entities import MegaOutPort
|
from .entities import MegaOutPort, BaseMegaEntity, safe_int
|
||||||
|
|
||||||
from .hub import MegaD
|
from .hub import MegaD
|
||||||
from .const import (
|
from .const import (
|
||||||
@@ -25,11 +39,19 @@ from .const import (
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
CONF_CUSTOM,
|
CONF_CUSTOM,
|
||||||
CONF_SKIP,
|
CONF_SKIP,
|
||||||
|
CONF_LED,
|
||||||
|
CONF_WS28XX,
|
||||||
|
CONF_PORTS,
|
||||||
|
CONF_WHITE_SEP,
|
||||||
|
CONF_SMOOTH,
|
||||||
|
CONF_ORDER,
|
||||||
|
CONF_CHIP,
|
||||||
|
RGB,
|
||||||
)
|
)
|
||||||
from .tools import int_ignore
|
from .tools import int_ignore, map_reorder_rgb
|
||||||
|
|
||||||
lg = logging.getLogger(__name__)
|
lg = logging.getLogger(__name__)
|
||||||
|
SCAN_INTERVAL = timedelta(seconds=5)
|
||||||
|
|
||||||
# Validation of the user's configuration
|
# Validation of the user's configuration
|
||||||
_EXTENDED = {
|
_EXTENDED = {
|
||||||
@@ -52,32 +74,286 @@ PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||||
lg.warning('mega integration does not support yaml for lights, please use UI configuration')
|
lg.warning(
|
||||||
|
"mega integration does not support yaml for lights, please use UI configuration"
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices
|
||||||
|
):
|
||||||
mid = config_entry.data[CONF_ID]
|
mid = config_entry.data[CONF_ID]
|
||||||
hub: MegaD = hass.data['mega'][mid]
|
hub: MegaD = hass.data["mega"][mid]
|
||||||
devices = []
|
devices = []
|
||||||
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {})
|
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
|
||||||
for port, cfg in config_entry.data.get('light', {}).items():
|
skip = []
|
||||||
|
if CONF_LED in customize:
|
||||||
|
for entity_id, conf in customize[CONF_LED].items():
|
||||||
|
ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)]
|
||||||
|
skip.extend(ports)
|
||||||
|
devices.append(
|
||||||
|
MegaRGBW(
|
||||||
|
mega=hub,
|
||||||
|
port=ports,
|
||||||
|
name=entity_id,
|
||||||
|
customize=conf,
|
||||||
|
id_suffix=entity_id,
|
||||||
|
config_entry=config_entry,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for port, cfg in config_entry.data.get("light", {}).items():
|
||||||
port = int_ignore(port)
|
port = int_ignore(port)
|
||||||
c = customize.get(mid, {}).get(port, {})
|
c = customize.get(port, {})
|
||||||
if c.get(CONF_SKIP, False) or c.get(CONF_DOMAIN, 'light') != 'light':
|
if (
|
||||||
|
c.get(CONF_SKIP, False)
|
||||||
|
or port in skip
|
||||||
|
or c.get(CONF_DOMAIN, "light") != "light"
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
for data in cfg:
|
for data in cfg:
|
||||||
hub.lg.debug(f'add light on port %s with data %s', port, data)
|
hub.lg.debug(f"add light on port %s with data %s", port, data)
|
||||||
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
|
light = MegaLight(mega=hub, port=port, config_entry=config_entry, **data)
|
||||||
if '<' in light.name:
|
if "<" in light.name:
|
||||||
continue
|
continue
|
||||||
devices.append(light)
|
devices.append(light)
|
||||||
|
|
||||||
async_add_devices(devices)
|
async_add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
class MegaLight(MegaOutPort, LightEntity):
|
class MegaLight(MegaOutPort, LightEntity):
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
return (SUPPORT_BRIGHTNESS if self.dimmer else 0) | (
|
||||||
|
SUPPORT_TRANSITION if self.dimmer else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MegaRGBW(LightEntity, BaseMegaEntity):
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self._is_on = None
|
||||||
|
self._brightness = None
|
||||||
|
self._hs_color = None
|
||||||
|
self._rgb_color: tuple[int, int, int] | None = None
|
||||||
|
self._white_value = None
|
||||||
|
self._task: asyncio.Task = None
|
||||||
|
self._restore = None
|
||||||
|
self.smooth: timedelta = self.customize[CONF_SMOOTH]
|
||||||
|
self._color_order = self.customize.get(CONF_ORDER, "rgb")
|
||||||
|
self._last_called: float = 0
|
||||||
|
self._max_values = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def max_values(self) -> list:
|
||||||
|
if self._max_values is None:
|
||||||
|
if self.is_ws:
|
||||||
|
self._max_values = [255] * 4
|
||||||
|
else:
|
||||||
|
self._max_values = [
|
||||||
|
255 if isinstance(x, int) else 4095 for x in self.port
|
||||||
|
]
|
||||||
|
return self._max_values
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chip(self) -> int:
|
||||||
|
return self.customize.get(CONF_CHIP, 100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ws(self):
|
||||||
|
return self.customize.get(CONF_WS28XX)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_color_modes(self) -> set[ColorMode] | set[str] | None:
|
||||||
|
return {
|
||||||
|
ColorMode.BRIGHTNESS,
|
||||||
|
ColorMode.RGB if len(self.port) != 4 else ColorMode.RGBW,
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def color_mode(self) -> ColorMode | str | None:
|
||||||
|
if len(self.port) == 4:
|
||||||
|
return ColorMode.RGBW
|
||||||
|
else:
|
||||||
|
return ColorMode.RGB
|
||||||
|
|
||||||
|
@property
|
||||||
|
def white_value(self):
|
||||||
|
# if self.supported_features & SUPPORT_WHITE_VALUE:
|
||||||
|
return float(self.get_attribute("white_value", 0))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgb_color(self) -> tuple[int, int, int] | None:
|
||||||
|
return self._rgb_color
|
||||||
|
|
||||||
|
@property
|
||||||
|
def rgbw_color(self) -> tuple[int, int, int, int] | None:
|
||||||
|
if self._white_value is not None and self._rgb_color is not None:
|
||||||
|
return (*self._rgb_color, self._white_value)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brightness(self):
|
||||||
|
return float(self.get_attribute("brightness", 0))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hs_color(self):
|
||||||
|
return self.get_attribute("hs_color", [0, 0])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self):
|
||||||
|
return self.get_attribute("is_on", False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def supported_features(self):
|
def supported_features(self):
|
||||||
return SUPPORT_BRIGHTNESS if self.dimmer else 0
|
return LightEntityFeature.TRANSITION
|
||||||
|
|
||||||
|
def get_rgbw(self):
|
||||||
|
if not self.is_on:
|
||||||
|
return [0 for x in range(len(self.port))] if not self.is_ws else [0] * 3
|
||||||
|
rgb = colorsys.hsv_to_rgb(
|
||||||
|
self.hs_color[0] / 360, self.hs_color[1] / 100, self.brightness / 255
|
||||||
|
)
|
||||||
|
rgb = [x for x in rgb]
|
||||||
|
if self.white_value is not None:
|
||||||
|
white = self.white_value
|
||||||
|
if not self.customize.get(CONF_WHITE_SEP):
|
||||||
|
white = white * (self.brightness / 255)
|
||||||
|
rgb.append(white / 255)
|
||||||
|
rgb = [round(x * self.max_values[i]) for i, x in enumerate(rgb)]
|
||||||
|
if self.is_ws:
|
||||||
|
# восстанавливаем мэпинг
|
||||||
|
rgb = map_reorder_rgb(rgb, RGB, self._color_order)
|
||||||
|
return rgb
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs):
|
||||||
|
if (time.time() - self._last_called) < 0.1:
|
||||||
|
return
|
||||||
|
self._last_called = time.time()
|
||||||
|
self.lg.debug(f"turn on %s with kwargs %s", self.entity_id, kwargs)
|
||||||
|
if self._restore is not None:
|
||||||
|
self._restore.update(kwargs)
|
||||||
|
kwargs = self._restore
|
||||||
|
self._restore = None
|
||||||
|
_before = self.get_rgbw()
|
||||||
|
self._is_on = True
|
||||||
|
if self._task is not None:
|
||||||
|
self._task.cancel()
|
||||||
|
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs):
|
||||||
|
if (time.time() - self._last_called) < 0.1:
|
||||||
|
return
|
||||||
|
self._last_called = time.time()
|
||||||
|
self._restore = {
|
||||||
|
"hs_color": self.hs_color,
|
||||||
|
"brightness": self.brightness,
|
||||||
|
"white_value": self.white_value,
|
||||||
|
}
|
||||||
|
_before = self.get_rgbw()
|
||||||
|
self._is_on = False
|
||||||
|
if self._task is not None:
|
||||||
|
self._task.cancel()
|
||||||
|
self._task = asyncio.create_task(self.set_color(_before, **kwargs))
|
||||||
|
|
||||||
|
async def set_color(self, _before, **kwargs):
|
||||||
|
transition = kwargs.get("transition")
|
||||||
|
update_state = transition is not None and transition > 3
|
||||||
|
_after = None
|
||||||
|
for item, value in kwargs.items():
|
||||||
|
setattr(self, f"_{item}", value)
|
||||||
|
if item == "rgb_color":
|
||||||
|
_after = map_reorder_rgb(value, RGB, self._color_order)
|
||||||
|
self._hs_color = colorsys.rgb_to_hsv(*value)
|
||||||
|
_after = _after or self.get_rgbw()
|
||||||
|
self._rgb_color = map_reorder_rgb(tuple(_after[:3]), self._color_order, RGB)
|
||||||
|
if transition is None:
|
||||||
|
transition = self.smooth.total_seconds()
|
||||||
|
ratio = self.calc_speed_ratio(_before, _after)
|
||||||
|
transition = transition * ratio
|
||||||
|
self.async_write_ha_state()
|
||||||
|
ports = self.port if not self.is_ws else self.port * 3
|
||||||
|
config = [(port, _before[i], _after[i]) for i, port in enumerate(ports)]
|
||||||
|
try:
|
||||||
|
await self.mega.smooth_dim(
|
||||||
|
*config,
|
||||||
|
time=transition,
|
||||||
|
ws=self.is_ws,
|
||||||
|
jitter=50,
|
||||||
|
updater=partial(self._update_from_rgb, update_state=update_state),
|
||||||
|
can_smooth_hardware=self.can_smooth_hardware,
|
||||||
|
max_values=self.max_values,
|
||||||
|
chip=self.chip,
|
||||||
|
)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
self.lg.exception("while dimming")
|
||||||
|
|
||||||
|
async def async_will_remove_from_hass(self) -> None:
|
||||||
|
await super().async_will_remove_from_hass()
|
||||||
|
if self._task is not None:
|
||||||
|
self._task.cancel()
|
||||||
|
|
||||||
|
def _update_from_rgb(self, rgbw, update_state=False):
|
||||||
|
if len(self.port) == 4:
|
||||||
|
w = rgbw[-1]
|
||||||
|
rgb = rgbw[:3]
|
||||||
|
else:
|
||||||
|
w = None
|
||||||
|
rgb = rgbw
|
||||||
|
if self.is_ws:
|
||||||
|
rgb = map_reorder_rgb(rgb, self._color_order, RGB)
|
||||||
|
h, s, v = colorsys.rgb_to_hsv(
|
||||||
|
*[x / self.max_values[i] for i, x in enumerate(rgb)]
|
||||||
|
)
|
||||||
|
h *= 360
|
||||||
|
s *= 100
|
||||||
|
v *= 255
|
||||||
|
self._hs_color = [h, s]
|
||||||
|
if self.is_on:
|
||||||
|
self._brightness = v
|
||||||
|
if w is not None:
|
||||||
|
if not self.customize.get(CONF_WHITE_SEP):
|
||||||
|
w = w / (self._brightness / 255)
|
||||||
|
else:
|
||||||
|
w = w
|
||||||
|
w = w / (self.max_values[-1] / 255)
|
||||||
|
self._white_value = w
|
||||||
|
# print(f'updated state {self.hs_color=} {self.brightness=}')
|
||||||
|
if update_state:
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""
|
||||||
|
Эта штука нужна для синхронизации статуса вкл/выкл с реальностью. Если все цвета сброшены в ноль, значит мега
|
||||||
|
рестартнулась и не запомнила настройки, поэтому извещаем HA о выключении
|
||||||
|
Если вручную править цвет на стороне меги, тут изменения отражаться не будут
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.enabled:
|
||||||
|
return
|
||||||
|
rgbw = []
|
||||||
|
for x in self.port:
|
||||||
|
data = self.coordinator.data
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return
|
||||||
|
data = data.get(x, None)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = data.get("value")
|
||||||
|
data = safe_int(data)
|
||||||
|
if data is None:
|
||||||
|
return
|
||||||
|
rgbw.append(data)
|
||||||
|
if sum(rgbw) == 0:
|
||||||
|
self._is_on = False
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
def calc_speed_ratio(self, _before, _after):
|
||||||
|
ret = None
|
||||||
|
for i, x in enumerate(_before):
|
||||||
|
r = abs(x - _after[i]) / self.max_values[i]
|
||||||
|
if ret is None:
|
||||||
|
ret = r
|
||||||
|
else:
|
||||||
|
ret = max([r, ret])
|
||||||
|
return ret
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"domain": "mega",
|
"domain": "mega",
|
||||||
"name": "mega",
|
"name": "mega",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mega_hacs",
|
"documentation": "https://github.com/andvikt/mega_hacs",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"beautifulsoup4",
|
"beautifulsoup4",
|
||||||
"lxml"
|
"lxml"
|
||||||
@@ -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": "v0.6.1b1"
|
"version": "v1.1.8b4"
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
"""Platform for light integration."""
|
"""Platform for light integration."""
|
||||||
import logging
|
import logging
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
import struct
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
PLATFORM_SCHEMA as SENSOR_SCHEMA,
|
PLATFORM_SCHEMA as SENSOR_SCHEMA, SensorEntity, SensorDeviceClass
|
||||||
DEVICE_CLASS_TEMPERATURE,
|
|
||||||
DEVICE_CLASS_HUMIDITY
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@@ -19,7 +18,8 @@ from homeassistant.const import (
|
|||||||
from homeassistant.core import HomeAssistant
|
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
|
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_FILL_NA
|
||||||
from .hub import MegaD
|
from .hub import MegaD
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -38,8 +38,8 @@ UNITS = {
|
|||||||
HUM: '%'
|
HUM: '%'
|
||||||
}
|
}
|
||||||
CLASSES = {
|
CLASSES = {
|
||||||
TEMP: DEVICE_CLASS_TEMPERATURE,
|
TEMP: SensorDeviceClass.TEMPERATURE,
|
||||||
HUM: DEVICE_CLASS_HUMIDITY
|
HUM: SensorDeviceClass.HUMIDITY
|
||||||
}
|
}
|
||||||
# Validation of the user's configuration
|
# Validation of the user's configuration
|
||||||
_ITEM = {
|
_ITEM = {
|
||||||
@@ -83,11 +83,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
|
|||||||
mid = config_entry.data[CONF_ID]
|
mid = config_entry.data[CONF_ID]
|
||||||
hub: MegaD = hass.data['mega'][mid]
|
hub: MegaD = hass.data['mega'][mid]
|
||||||
devices = []
|
devices = []
|
||||||
|
customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
|
||||||
for tp in ['sensor', 'i2c']:
|
for tp in ['sensor', 'i2c']:
|
||||||
for port, cfg in config_entry.data.get(tp, {}).items():
|
for port, cfg in config_entry.data.get(tp, {}).items():
|
||||||
port = int_ignore(port)
|
port = int_ignore(port)
|
||||||
|
c = customize.get(port, {})
|
||||||
|
if c.get(CONF_SKIP):
|
||||||
|
hub.skip_ports |= {port}
|
||||||
|
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,
|
||||||
@@ -101,32 +106,131 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
|
|||||||
async_add_devices(devices)
|
async_add_devices(devices)
|
||||||
|
|
||||||
|
|
||||||
class MegaI2C(MegaPushEntity):
|
class FilterBadValues(MegaPushEntity, SensorEntity):
|
||||||
|
|
||||||
def __init__(self, *args, device_class: str, params: dict, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self._device_class = device_class
|
self._prev_value = None
|
||||||
self._params = tuple(params.items())
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def device_class(self):
|
def filter_value(self, value):
|
||||||
return self._device_class
|
try:
|
||||||
|
if value \
|
||||||
|
in self.filter_values \
|
||||||
|
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._prev_value is not None
|
||||||
|
and self.filter_scale is not None
|
||||||
|
and (
|
||||||
|
abs(value - self._prev_value) / self._prev_value > self.filter_scale
|
||||||
|
)
|
||||||
|
):
|
||||||
|
if self.fill_na == 'last':
|
||||||
|
value = self._prev_value
|
||||||
|
else:
|
||||||
|
value = None
|
||||||
|
self._prev_value = value
|
||||||
|
return value
|
||||||
|
except Exception as exc:
|
||||||
|
lg.exception(f'while parsing value')
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def filter_values(self):
|
||||||
# self.lg.debug(f'get % all states: %', self._params, self.mega.values)
|
return self.customize.get(CONF_FILTER_VALUES, self.mega.customize.get(CONF_FILTER_VALUES, []))
|
||||||
return self.mega.values.get(self._params)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_class(self):
|
def filter_scale(self):
|
||||||
return self._device_class
|
return self.customize.get(CONF_FILTER_SCALE, self.mega.customize.get(CONF_FILTER_SCALE, None))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_low(self):
|
||||||
|
return self.customize.get(CONF_FILTER_LOW, self.mega.customize.get(CONF_FILTER_LOW, None))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def filter_high(self):
|
||||||
|
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 Mega1WSensor(MegaPushEntity):
|
class MegaI2C(FilterBadValues):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
unit_of_measurement,
|
*args,
|
||||||
device_class,
|
device_class: str,
|
||||||
|
params: dict,
|
||||||
|
unit_of_measurement: str = None,
|
||||||
|
**kwargs
|
||||||
|
):
|
||||||
|
self._device_class = device_class
|
||||||
|
self._params = tuple(params.items())
|
||||||
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def customize(self):
|
||||||
|
ret = super().customize
|
||||||
|
_old = ret.get(self.id_suffix)
|
||||||
|
if _old is not None:
|
||||||
|
ret = ret.copy()
|
||||||
|
ret.update(_old)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self):
|
||||||
|
attrs = super().extra_state_attributes or {}
|
||||||
|
attrs.update({
|
||||||
|
'i2c_id': self.id_suffix,
|
||||||
|
})
|
||||||
|
return attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_unit_of_measurement(self):
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self):
|
||||||
|
try:
|
||||||
|
ret = self.mega.values.get(self._params)
|
||||||
|
if self.customize.get(CONF_HEX_TO_FLOAT):
|
||||||
|
try:
|
||||||
|
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
|
||||||
|
except:
|
||||||
|
self.lg.warning(f'could not convert {ret} form hex to float')
|
||||||
|
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
|
||||||
|
try:
|
||||||
|
ret = float(ret)
|
||||||
|
if tmpl is not None and self.hass is not None:
|
||||||
|
tmpl.hass = self.hass
|
||||||
|
ret = tmpl.async_render({'value': ret})
|
||||||
|
except:
|
||||||
|
ret = ret
|
||||||
|
ret = self.filter_value(ret)
|
||||||
|
if ret is not None:
|
||||||
|
return str(ret)
|
||||||
|
except Exception:
|
||||||
|
lg.exception('while getting value')
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_class(self):
|
||||||
|
return self._device_class
|
||||||
|
|
||||||
|
|
||||||
|
class Mega1WSensor(FilterBadValues):
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
unit_of_measurement=None,
|
||||||
|
device_class=None,
|
||||||
key=None,
|
key=None,
|
||||||
*args,
|
*args,
|
||||||
**kwargs
|
**kwargs
|
||||||
@@ -138,14 +242,15 @@ class Mega1WSensor(MegaPushEntity):
|
|||||||
: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)
|
||||||
|
self.prev_value = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unit_of_measurement(self):
|
def native_unit_of_measurement(self):
|
||||||
_u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None)
|
_u = self.customize.get(CONF_UNIT_OF_MEASUREMENT, None)
|
||||||
if _u is None:
|
if _u is None:
|
||||||
return self._unit_of_measurement
|
return self._unit_of_measurement
|
||||||
@@ -161,7 +266,7 @@ class Mega1WSensor(MegaPushEntity):
|
|||||||
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):
|
||||||
@@ -176,43 +281,67 @@ class Mega1WSensor(MegaPushEntity):
|
|||||||
return self._device_class
|
return self._device_class
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def native_value(self):
|
||||||
ret = None
|
|
||||||
if self.key:
|
|
||||||
try:
|
|
||||||
ret = self.mega.values.get(self.port, {})
|
|
||||||
if isinstance(ret, dict):
|
|
||||||
ret = ret.get('value', {})
|
|
||||||
if isinstance(ret, dict):
|
|
||||||
ret = ret.get(self.key)
|
|
||||||
except:
|
|
||||||
self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
ret = self.mega.values.get(self.port, {}).get('value')
|
|
||||||
if ret is None and self._state is not None:
|
|
||||||
ret = self._state.state
|
|
||||||
try:
|
try:
|
||||||
ret = float(ret)
|
|
||||||
ret = str(ret)
|
|
||||||
except:
|
|
||||||
ret = None
|
ret = None
|
||||||
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
|
if not hasattr(self, 'key'):
|
||||||
if tmpl is not None and self.hass is not None:
|
return None
|
||||||
tmpl.hass = self.hass
|
if self.key:
|
||||||
ret = tmpl.async_render({'value': ret})
|
try:
|
||||||
return ret
|
ret = self.mega.values.get(self.port, {})
|
||||||
|
if isinstance(ret, dict):
|
||||||
|
ret = ret.get('value', {})
|
||||||
|
if isinstance(ret, dict):
|
||||||
|
ret = ret.get(self.key)
|
||||||
|
except:
|
||||||
|
self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
ret = self.mega.values.get(self.port, {}).get('value')
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
ret = float(ret)
|
||||||
|
ret = str(ret)
|
||||||
|
except:
|
||||||
|
self.lg.debug(f'could not convert to float "{ret}"')
|
||||||
|
ret = self.prev_value
|
||||||
|
if self.customize.get(CONF_HEX_TO_FLOAT):
|
||||||
|
try:
|
||||||
|
ret = struct.unpack('!f', bytes.fromhex(ret))[0]
|
||||||
|
except:
|
||||||
|
self.lg.warning(f'could not convert {ret} form hex to float')
|
||||||
|
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
|
||||||
|
try:
|
||||||
|
ret = float(ret)
|
||||||
|
if tmpl is not None and self.hass is not None:
|
||||||
|
tmpl.hass = self.hass
|
||||||
|
ret = tmpl.async_render({'value': ret})
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
ret = self.filter_value(ret)
|
||||||
|
self.prev_value = ret
|
||||||
|
if ret is not None:
|
||||||
|
return str(ret)
|
||||||
|
except Exception:
|
||||||
|
lg.exception('while parsing state')
|
||||||
|
return None
|
||||||
|
|
||||||
@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
|
||||||
|
|
||||||
|
|
||||||
_constructors = {
|
_constructors = {
|
||||||
'sensor': Mega1WSensor,
|
'sensor': Mega1WSensor,
|
||||||
'i2c': MegaI2C,
|
'i2c': MegaI2C,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ class PriorityLock(asyncio.Lock):
|
|||||||
finally:
|
finally:
|
||||||
self.release()
|
self.release()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _loop(self):
|
||||||
|
return asyncio.get_event_loop()
|
||||||
|
|
||||||
async def acquire(self, priority=0) -> bool:
|
async def acquire(self, priority=0) -> bool:
|
||||||
"""Acquire a lock.
|
"""Acquire a lock.
|
||||||
|
|
||||||
@@ -115,3 +119,10 @@ class PriorityLock(asyncio.Lock):
|
|||||||
# taken already, will hit this again and wake up a new waiter.
|
# taken already, will hit this again and wake up a new waiter.
|
||||||
if not fut.done():
|
if not fut.done():
|
||||||
fut.set_result(True)
|
fut.set_result(True)
|
||||||
|
|
||||||
|
|
||||||
|
def map_reorder_rgb(rgb: list, from_: str, to_: str):
|
||||||
|
if from_ == to_:
|
||||||
|
return rgb
|
||||||
|
mapping = [from_.index(x) for x in to_]
|
||||||
|
return [rgb[x] for x in mapping]
|
||||||
|
|||||||
58
docs/blueprints.md
Normal file
58
docs/blueprints.md
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
**blueprints** - это удобные шаблоны автоматизаций, которые помогают строить автоматизацию из
|
||||||
|
интерфейса и ими легко делиться. Все ваши шаблоны доступны из специального меню.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/blueprints/)
|
||||||
|
|
||||||
|
[Официальная документация по blueprints](https://www.home-assistant.io/docs/blueprint/)
|
||||||
|
|
||||||
|
## Общее
|
||||||
|
Здесь я делюсь шаблонами, в которых используются события из моей интеграции.
|
||||||
|
|
||||||
|
Если вы хотите сделать что-то подобное своими руками, то можно использовать мои шаблоны как отправную точку.
|
||||||
|
|
||||||
|
Во всех шаблонах в качестве триггера используется событие **mega.binary** и доступен выбор типа,
|
||||||
|
[подробное описание типов здесь](events.md#binary).
|
||||||
|
|
||||||
|
## Включить что-то
|
||||||
|
Этот шаблон лучше всего подходит для включения сценариев/сцен или любых других объектов по нажатию какой-то кнопки или
|
||||||
|
обнаружению движения.
|
||||||
|
|
||||||
|
!!! note "Движение"
|
||||||
|
Датчики движения - это такие же *binary_sensor* как и обычные выключатели. В зависимости от настроек контроллера
|
||||||
|
будут приходить события либо типа **single** (если настроен режим click), либо **press**
|
||||||
|
|
||||||
|
Опционально так же доступна настройка автоматического выключения по таймеру, если указан 0 (по умолчанию), таймер не
|
||||||
|
будет использован.
|
||||||
|
|
||||||
|
Опционально доступен так же *блокирующий объект* и *период блокировки*. Например, если в одной комнате с датчиком
|
||||||
|
движения есть выключатель, тогда его можно указать как *блокирующий объект* и в течении *периода блокировки*
|
||||||
|
после нажатия выключателя события с датчика движения будут игнорироваться.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgist.github.com%2Fandvikt%2Fb78459f4f43862d04c7fbba20d6893c7)
|
||||||
|
|
||||||
|
[Исходный код](https://gist.github.com/andvikt/b78459f4f43862d04c7fbba20d6893c7)
|
||||||
|
## Переключить состояние
|
||||||
|
Классическое управление светом с кнопки без фиксации: нажали кнопку - свет выключился, если он сейчас включен, и наоборот.
|
||||||
|
Если вам нужно управлять несколькими светильниками, то необходимо будет
|
||||||
|
[создать группу света](https://www.home-assistant.io/integrations/light.group/)
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgist.github.com%2Fandvikt%2Fefb48535b1b9d998fe3dbe9a3efcea2c)
|
||||||
|
|
||||||
|
[Исходный код](https://gist.github.com/andvikt/efb48535b1b9d998fe3dbe9a3efcea2c)
|
||||||
|
## Выключатель с фиксацией
|
||||||
|
Если выбран тип "нестрогий", то при каждом переключении состояния выключателя состоянии целевого объекта так же будет
|
||||||
|
меняться. Этот режим рекомендуется, тк в случае переключения состояния с сервера, в случае со строгим типом будет
|
||||||
|
"рассинхрон" - вам придется сначала выключатель привести в соответствие с текущим состоянием света.
|
||||||
|
|
||||||
|
Если выбран тип "строгий", то будет строгое соответсвие состояний, те выкл==выкл и наоборот.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgist.github.com%2Fandvikt%2F9addf966db75d0964143177963f40bb9)
|
||||||
|
|
||||||
|
[Исходный код](https://gist.github.com/andvikt/9addf966db75d0964143177963f40bb9)
|
||||||
|
## Универсальный шаблон
|
||||||
|
Универсальный шаблон, с помощью которого можно выбрать любое событие меги, привязать
|
||||||
|
к нему набор произвольных действий
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fgist.github.com%2Fandvikt%2Fbe1f683d308050b8972f9efa8aec465f)
|
||||||
|
|
||||||
|
[Исходный код](https://gist.github.com/andvikt/be1f683d308050b8972f9efa8aec465f)
|
||||||
15
docs/debug.md
Normal file
15
docs/debug.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[Сообщить о проблеме](https://github.com/andvikt/mega_hacs/issues/new?assignees=&labels=&template=bug-report.md&title=){ .md-button .md-button--primary }
|
||||||
|
|
||||||
|
В первую очередь проверьте лог на наличие ошибок, доступ к логу возможен по кнопке ниже.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/logs/)
|
||||||
|
|
||||||
|
Так же будет очень полезно прикладывать детальный лог, который можно включить в конфиге так:
|
||||||
|
```yaml
|
||||||
|
logger:
|
||||||
|
default: info
|
||||||
|
logs:
|
||||||
|
custom_components.mega: debug
|
||||||
|
```
|
||||||
|
Для просмотра логов рекомендуется использовать [logviewer](https://github.com/hassio-addons/addon-log-viewer)
|
||||||
|
|
||||||
66
docs/events.md
Normal file
66
docs/events.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
Для быстрого старта рекомендую попробовать [мои шаблоны автоматизаций](blueprints.md)
|
||||||
|
|
||||||
|
## mega.binary {: #binary}
|
||||||
|
События можно использовать только в автоматизациях как триггер типа *event*
|
||||||
|
```yaml
|
||||||
|
- alias: some long click
|
||||||
|
trigger:
|
||||||
|
- platform: event
|
||||||
|
event_type: mega.binary
|
||||||
|
event_data:
|
||||||
|
entity_id: binary_sensor.some_id
|
||||||
|
type: long
|
||||||
|
action:
|
||||||
|
- service: light.toggle
|
||||||
|
entity_id: light.some_light
|
||||||
|
```
|
||||||
|
!!! note "Возможные варианты поля type"
|
||||||
|
- **press**: замыкание
|
||||||
|
- **release**: размыкание (с гарантией, что не было долгого нажатия)
|
||||||
|
|
||||||
|
*Эти типы доступны только в режиме click (настраивается на контроллере):*
|
||||||
|
|
||||||
|
- **long**: долгое нажатие
|
||||||
|
- **long_release**: размыкание после долгого нажатия
|
||||||
|
- **single**: одинарный клик (в режиме кликов)
|
||||||
|
- **double**: двойной клик
|
||||||
|
|
||||||
|
## mega.sensor
|
||||||
|
Этот вид событий более "технический", им имеет смысл пользоваться только если функциональности *mega.binary* не
|
||||||
|
достаточно.
|
||||||
|
```yaml
|
||||||
|
# событие при перезагрузке меги
|
||||||
|
- alias: mega restart
|
||||||
|
trigger:
|
||||||
|
- platform: event
|
||||||
|
event_type: mega.sensor
|
||||||
|
event_data:
|
||||||
|
st: 1
|
||||||
|
action:
|
||||||
|
# какой-то экшн
|
||||||
|
# Пример события с полями как есть прямо из меги
|
||||||
|
- alias: some double click
|
||||||
|
trigger:
|
||||||
|
- platform: event
|
||||||
|
event_type: mega.sensor
|
||||||
|
event_data:
|
||||||
|
pt: 1
|
||||||
|
click: 2
|
||||||
|
action:
|
||||||
|
- service: light.toggle
|
||||||
|
entity_id: light.some_light
|
||||||
|
```
|
||||||
|
!!! note "События могут содержать следующие поля в event_data"
|
||||||
|
- **mega_id**: id как в конфиге HA
|
||||||
|
- **pt**: номер порта
|
||||||
|
- **cnt**: счетчик срабатываний
|
||||||
|
- **mdid**: id как в конфиге контроллера
|
||||||
|
- **click**: клик (подробнее в документации меги)
|
||||||
|
- **port**: номер порта
|
||||||
|
|
||||||
|
|
||||||
|
## Отладка
|
||||||
|
Чтобы понять, какие события приходят, лучше всего воспользоваться панелью разработчика (кнопка ниже) и подписаться
|
||||||
|
на вкладке события на событие `mega.binary` или `mega.sensor`, понажимать физические кнопки на меге.
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/developer_events/)
|
||||||
64
docs/http.md
Normal file
64
docs/http.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
Контроллер оповещает сервер о своих событиях, например, нажали кнопку выключателя или сработал датчик движения,
|
||||||
|
для этого в интеграции реализован http-сервер, для его работы необходимо прописать
|
||||||
|
в настройках меги следующие параметры:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
srv: "192.168.1.4:8123" # ip:port вашего HA
|
||||||
|
script: "mega" # это api интеграции, к которому будет обращаться контроллер
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note "Внимание!"
|
||||||
|
Не используйте **srv loop** на контроллере - это может приводить к ложным срабатываниям входов. Вместо srv loop
|
||||||
|
интеграция будет сама обновлять все состояния портов с заданным интервалом
|
||||||
|
|
||||||
|
За события будут отвечать объекты типа *binary_sensor* - их статус будет меняться на **on** при замыкании
|
||||||
|
контакта, на **off** при размыкании, а так же для более сложного контроля (двойные, долгие нажатия) предусмотрены
|
||||||
|
события с типом *mega.binary*, [об этом подробнее в разделе события](events.md)
|
||||||
|
|
||||||
|
Так же вы можете [воспользоваться моими шаблонами автоматизаций](blueprints.md) для быстрого понимания, как всем этим
|
||||||
|
пользоваться.
|
||||||
|
|
||||||
|
## Ответ на входящие события от контроллера
|
||||||
|
|
||||||
|
Контроллер ожидает ответ от сервера, который может быть сценарием (по умолчанию интеграция отвечает `d`, что означает
|
||||||
|
запустить то что прописано в поле act в настройках порта).
|
||||||
|
|
||||||
|
*Внимание!* По умолчанию в настройках интеграции стоит опция `имитация ответа` - это означает, что сервер вместо ответа
|
||||||
|
делает запрос к меге с необходимой командой - это вынужденная мера, тк встроенный в HA сервер разбивает пакет на части,
|
||||||
|
а контроллер не работает с такими пакетами. В целом, `имитация ответа` полностью закрывает эту проблему, единственный
|
||||||
|
недостаток - это небольшая задержка в ответе.
|
||||||
|
|
||||||
|
Для максимальной скорости реакции, можно воспользоваться
|
||||||
|
[аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy), подробности в документации аддона.
|
||||||
|
|
||||||
|
[Поддерживаются шаблоны HA.](yaml.md#binary) Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не
|
||||||
|
умеет).
|
||||||
|
|
||||||
|
## Отладка шаблонов {: #temp-debug }
|
||||||
|
Отладку шаблонов рекомендуется проводить в специальном меню HA, которое находится в `Панель разработчика` - `Шаблоны`
|
||||||
|
|
||||||
|
Вот пример, с которого можно начать:
|
||||||
|
```yaml
|
||||||
|
{## Переменные, которые передает контроллер, указываются только в тесте ##}
|
||||||
|
{% set m = 1%}
|
||||||
|
{% set pt = 2%}
|
||||||
|
{% set mdid = 'mega'%}
|
||||||
|
{## Шаблон ответа ##}
|
||||||
|
{% if m in [0, 1] %}d{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Отладка ответов http-сервера {: #http-response }
|
||||||
|
Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли HA:
|
||||||
|
```shell
|
||||||
|
curl -v -X GET 'http://localhost:8123/mega?pt=5&m=1&mdid=mymega1'
|
||||||
|
```
|
||||||
|
Где mymega1 - id устройства mega, который нужно узнать по url `http://192.168.1.14/sec/?cf=2`
|
||||||
|
|
||||||
|
При этом необходимо так же в настройках интеграции прописать хост, с которого вы будете обращаться,
|
||||||
|
[подробнее](yaml.md#allow_hosts)
|
||||||
|
|
||||||
|
И тогда можно с локальной машины делать запросы на ваш сервер HA:
|
||||||
|
```shell
|
||||||
|
curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1&mdid=mymega1'
|
||||||
|
```
|
||||||
|
В ответ будет приходить либо d, либо скрипт, который вы настроили
|
||||||
29
docs/i2c.md
Normal file
29
docs/i2c.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
I2C-датчики будут добавлены автоматически в HA с названием, соответствующим порту, типу и адресу
|
||||||
|
(если необходим), название и entity_id вы всегда можете поменять из интерфейса HA, [а также дополнительно их
|
||||||
|
кастомизировать с помощью yaml](yaml.md#sensors).
|
||||||
|
|
||||||
|
Как и все остальные датчики, i2c подчиняется единому интервалу обновления, который указывается в меню интеграции.
|
||||||
|
|
||||||
|
## Список i2c-датчиков, поддерживаемых интеграцией: {: #list}
|
||||||
|
!!! note ""
|
||||||
|
Под поддерживаемыми подразумеваются те датчики, у которых учтены все возможные
|
||||||
|
дополнительные значения, а так же корректно определены типы
|
||||||
|
|
||||||
|
Неподдерживаемые датчики все равно будут работать, но будет отображаться только основное значение (i2c_par=0),
|
||||||
|
а тип будет определен как общий, универсальный для всех датчиков.
|
||||||
|
|
||||||
|
- HTU21D/Si7021
|
||||||
|
- SHT31
|
||||||
|
- MAX44009
|
||||||
|
- BH1750
|
||||||
|
- TSL2591
|
||||||
|
- BMP180
|
||||||
|
- BME280
|
||||||
|
- T6703/T67xx
|
||||||
|
- MLX90614
|
||||||
|
- PTsensor
|
||||||
|
- MCP9600
|
||||||
|
- DPS368
|
||||||
|
- ADS1115/ADS1015
|
||||||
|
|
||||||
|
Так же заводите issue если какой-то датчик отсутсвует в этом списке, но поддерживается контроллером.
|
||||||
71
docs/index.md
Normal file
71
docs/index.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# MegaD HomeAssistant integration
|
||||||
|
[](https://github.com/custom-components/hacs)
|
||||||
|
[](https://yoomoney.ru/to/410013955329136)
|
||||||
|
<a class="github-button" href="https://github.com/andvikt/mega_hacs" data-icon="octicon-star" data-show-count="true" aria-label="Star andvikt/mega_hacs on GitHub">Star</a>
|
||||||
|
|
||||||
|
[Сообщить о проблеме](https://github.com/andvikt/mega_hacs/issues/new?assignees=&labels=&template=bug-report.md&title=){ .md-button .md-button--primary }
|
||||||
|
[Предложение об улучшении](https://github.com/andvikt/mega_hacs/issues/new?assignees=&labels=enhancement&template=enhance.md&title=){ .md-button .md-button--primary }
|
||||||
|
|
||||||
|
Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
|
||||||
|
|
||||||
|
|
||||||
|
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
|
||||||
|
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
|
||||||
|
|
||||||
|
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
|
||||||
|
|
||||||
|
## Основные особенности {: #mains }
|
||||||
|
- Настройка в [веб-интерфейсе](settings.md) + [yaml](yaml.md)
|
||||||
|
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
|
||||||
|
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
|
||||||
|
`sensor`)
|
||||||
|
- Поддержка rgb+w лент как с использованием диммеров, так и адресных лент на чипах ws28xx и подобных,
|
||||||
|
[подробнее про rgbw](yaml.md#rgb)
|
||||||
|
- Плавное диммирование для любых диммируемых объектов (в том числе с аппаратной поддержкой и без),
|
||||||
|
[подробнее про smooth](smooth.md)
|
||||||
|
- Возможность работы с несколькими megad
|
||||||
|
- Обратная связь по [http](http.md)
|
||||||
|
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
|
||||||
|
- Автоматическое добавление/изменение объектов после перезагрузки контроллера
|
||||||
|
- [События](events.md) на двойные/долгие нажатия
|
||||||
|
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
|
||||||
|
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
|
||||||
|
выполнении предыдущей.
|
||||||
|
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) в том числе несколько шиной (начиная с версии 0.4.1)
|
||||||
|
- поддержка расширителей MegaD-16I-XT, MegaD-16R-XT, MegaD-16PWM (начиная с версии 0.5.1)
|
||||||
|
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
|
||||||
|
[по ссылке](i2c.md) (начиная с версии 0.6.1)
|
||||||
|
|
||||||
|
## Установка {: #install}
|
||||||
|
Если вы уже раньше устанавливали HACS, то просто поищите в списке интеграций HACS MegaD, если нет, то сначла необходимо
|
||||||
|
установить HACS - это витрина сторонних интеграций. [Инструкция по установке](https://hacs.xyz/docs/installation/installation)
|
||||||
|
|
||||||
|
Далее внутри интерфейса HACS ищем MegaD: `HACS - Integrations - Explore`, в поиске ищем MegaD.
|
||||||
|
|
||||||
|
На этом установка не закончена, вам потребуется прописать настройки каждого контроллера, [подробнее](settings.md)
|
||||||
|
|
||||||
|
!!! note "Альтернативный способ установки"
|
||||||
|
Откройте терминал (стандартный аддон Terminal & SSH, если у вас есть supervisor, если нет то терминал вашей системы)
|
||||||
|
```shell
|
||||||
|
# из папки с конфигом
|
||||||
|
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
|
||||||
|
```
|
||||||
|
Не забываем перезагрузить HA
|
||||||
|
|
||||||
|
## Обновления
|
||||||
|
Обновления выполняются так же в меню HACS.
|
||||||
|
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
|
||||||
|
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
|
||||||
|
|
||||||
|
## Беты {: #beta }
|
||||||
|
Иногда я буду выпускать бета-версии, в стабильности которых пока не уверен и не готов раскатывать на всех, поэтому в целях тестирвоания
|
||||||
|
нужно принудительно включать поддержку бет, для этого зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
|
||||||
|
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
|
||||||
|
|
||||||
|
## Зависимости {: #deps }
|
||||||
|
Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
|
||||||
|
настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
|
||||||
|
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
|
||||||
|
|
||||||
|
Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
|
||||||
|
из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
|
||||||
32
docs/services.md
Normal file
32
docs/services.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
Все сервисы доступны в меню разработчика с описанием и примерами использования
|
||||||
|
```yaml
|
||||||
|
mega.save:
|
||||||
|
description: Сохраняет текущее состояние портов (?cmd=s)
|
||||||
|
fields:
|
||||||
|
mega_id:
|
||||||
|
description: ID меги, можно оставить пустым, тогда будут сохранены все зарегистрированные меги
|
||||||
|
example: "mega"
|
||||||
|
|
||||||
|
mega.get_port:
|
||||||
|
description: Запросить текущий статус порта (или всех)
|
||||||
|
fields:
|
||||||
|
mega_id:
|
||||||
|
description: ID меги, можно оставить пустым, тогда будут порты всех зарегистрированных мег
|
||||||
|
example: "mega"
|
||||||
|
port:
|
||||||
|
description: Номер порта (если не заполнять, будут запрошены все порты сразу)
|
||||||
|
example: 1
|
||||||
|
|
||||||
|
mega.run_cmd:
|
||||||
|
description: Выполнить любую произвольную команду
|
||||||
|
fields:
|
||||||
|
mega_id:
|
||||||
|
description: ID меги
|
||||||
|
example: "mega"
|
||||||
|
port:
|
||||||
|
description: Номер порта (это не порт, которым мы управляем, а порт с которого шлем команду)
|
||||||
|
example: 1
|
||||||
|
cmd:
|
||||||
|
description: Любая поддерживаемая мегой команда
|
||||||
|
example: "1:0"
|
||||||
|
```
|
||||||
16
docs/settings.md
Normal file
16
docs/settings.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[После успешной установки интеграции в HACS](index.md#install), необходимо настроить
|
||||||
|
каждый контроллер, проще всего сделать это по этой кнопке:
|
||||||
|
|
||||||
|
[](https://my.home-assistant.io/redirect/config_flow_start/?domain=mega)
|
||||||
|
|
||||||
|
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
|
||||||
|
|
||||||
|
В самой меге необходимо прописать настройки:
|
||||||
|
```yaml
|
||||||
|
srv: "192.168.1.4:8123" # ip:port вашего HA
|
||||||
|
script: "mega" # это api интеграции, к которому будет обращаться контроллер
|
||||||
|
```
|
||||||
|
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
|
||||||
|
|
||||||
|
При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
|
||||||
|
объекты`
|
||||||
50
docs/smooth.md
Normal file
50
docs/smooth.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
Начиная с версии `1.0.0` интеграция поддерживает плавные переходы. Функция реализована
|
||||||
|
как на аппаратном уровне, так и на программном.
|
||||||
|
|
||||||
|
Для аппаратной поддержки в настройках контроллера диммируемого порта необходимо включить опцию smooth.
|
||||||
|
|
||||||
|
В чем разница между аппаратной и программной реализацией? Контроллер на аппаратном уровне умеет медленно
|
||||||
|
менять значение pwm-порта, рекомендуется для всех портов с поддержкой этого режима использовать именно его,
|
||||||
|
тк будет обеспечена максимальная плавность для любого числа устройств одновременно.
|
||||||
|
Плавность программного диммирования ограничена ресурсами вашего сервера и скоростью ответа контроллера,
|
||||||
|
если вы будете довольно быстро (за пару секунд) диммировать сразу группу
|
||||||
|
света из нескольких светильков, то в программной реализации возможно увидеть скачки.
|
||||||
|
|
||||||
|
Тем не менее, pwm-расширитель не умеет аппаратно сглаживать диммирование, поэтому для него есть смысл воспользоваться
|
||||||
|
программной реализацией
|
||||||
|
|
||||||
|
Для запуска плавного перехода можно воспользоваться штатными сервисами, например:
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
service: light.turn_on
|
||||||
|
entity_id: light.some_light
|
||||||
|
data:
|
||||||
|
# свет будет плавно включаться в течении 30 секунд
|
||||||
|
brightness_pct: 50
|
||||||
|
transition: 10 # кол-во секунд на переход
|
||||||
|
```
|
||||||
|
Так же любые диммируемые каналы могут участвовать в сценах, а эти сцены в свою очередь будут поддерживать опцию transition:
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
service: scene.turn_on
|
||||||
|
target:
|
||||||
|
entity_id: scene.romantic
|
||||||
|
data:
|
||||||
|
transition: 2.5
|
||||||
|
```
|
||||||
|
|
||||||
|
Плавность реализована в любых диммируемых объектах: свет, rgb-ленты.
|
||||||
|
|
||||||
|
Кроме того, возможно установить плавность по-умолчанию (имеет смысл использовать на pwm-расширителе), для этого в yaml-конфиге
|
||||||
|
следует добавить опцию smooth:
|
||||||
|
```yaml
|
||||||
|
mega:
|
||||||
|
mega1:
|
||||||
|
10e1:
|
||||||
|
smooth: 1 # если указать, то порт будет диммироваться плавно (от 0 до 100% за <smooth> секунд)
|
||||||
|
# опцию smooth можно использовать и на обычном pwm-порте, но в этом мало необходимости, лучше использовать
|
||||||
|
# встроенный в контроллер механизм smooth
|
||||||
|
```
|
||||||
|
|
||||||
|
Для светодиодных лент smooth по умолчанию установлен в 1 секунду,
|
||||||
|
подробнее [тут](yaml.md#rgb)
|
||||||
269
docs/yaml.md
Normal file
269
docs/yaml.md
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
С помощью 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`, начинаем с
|
||||||
|
указания названия интеграции:
|
||||||
|
```yaml hl_lines="1"
|
||||||
|
mega:
|
||||||
|
megaid1:
|
||||||
|
10:
|
||||||
|
domain: switch
|
||||||
|
invert: true
|
||||||
|
megaid2:
|
||||||
|
14:
|
||||||
|
hex_to_float: true
|
||||||
|
```
|
||||||
|
Далее каждый новый контроллер добавляется с помощью указания его id, который вы
|
||||||
|
придумали при установке интеграции
|
||||||
|
```yaml hl_lines="2 6"
|
||||||
|
mega:
|
||||||
|
megaid1:
|
||||||
|
10:
|
||||||
|
domain: switch
|
||||||
|
invert: true
|
||||||
|
megaid2:
|
||||||
|
14:
|
||||||
|
hex_to_float: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Далее конфигурируются порты:
|
||||||
|
```yaml hl_lines="3 4 5 7 8"
|
||||||
|
mega:
|
||||||
|
megaid1:
|
||||||
|
10:
|
||||||
|
domain: switch
|
||||||
|
invert: true
|
||||||
|
megaid2:
|
||||||
|
14:
|
||||||
|
hex_to_float: true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параметры устройств
|
||||||
|
В зависимости от типа порта доступны разные параметры. Все параметры опциональные, в скобках приведены типы и дефолтные
|
||||||
|
значения.
|
||||||
|
### Стандартный набор параметров
|
||||||
|
Все устройства вне зависимости от типа
|
||||||
|
!!! note ""
|
||||||
|
- **skip** (bool, false): пропустить или нет. Если true - устройство будет исключено из списка
|
||||||
|
- **name** (str): имя, используется в интерфейсе
|
||||||
|
### Реле
|
||||||
|
!!! note ""
|
||||||
|
- **domain** (str, light): тип устройства. Можно выбрать light или switch
|
||||||
|
- **invert** (bool, false): инвертировать или нет.
|
||||||
|
### ds2413
|
||||||
|
Те же параметры, что у реле, но записываются иначе:
|
||||||
|
```yaml
|
||||||
|
10:
|
||||||
|
c6c439000000_a: #c6c439000000 - это адрес ds2413, a-первый канал
|
||||||
|
# параметры
|
||||||
|
c6c439000000_b: #b-второй канал
|
||||||
|
```
|
||||||
|
### Диммеры
|
||||||
|
!!! note ""
|
||||||
|
- **smooth** (float, 0): программное плавное диммирование. Это поле отвечает за кол-во секунд, за которое яркость
|
||||||
|
диммера набирает от 0 до 100%
|
||||||
|
- **range** (list[int, int], [0, 255]), *начиная с версии 1.1.0*: границы диммирования в абсолютных единицах 0..255. При диммировании 1%
|
||||||
|
будет равен левой границе, 100% - правой.
|
||||||
|
```yaml
|
||||||
|
range: [20, 200]
|
||||||
|
```
|
||||||
|
[Подробнее про плавное диммирование](smooth.md)
|
||||||
|
### MegaD-16R-XT, MegaD-16PWM
|
||||||
|
Порты расширителей MegaD-16R-XT, MegaD-16PWM конфигурируются аналогично обычным реле и диммерам, но адресация порта
|
||||||
|
выглядит так:
|
||||||
|
```yaml
|
||||||
|
33e1: # 33-основной порт, на котором сидит расширитель, e1-дополнительный порт расширителя
|
||||||
|
# стандартный конфиг порта
|
||||||
|
33e2:
|
||||||
|
```
|
||||||
|
### RGB+W {: #rgb}
|
||||||
|
Для настройки rgb(w) лент существует специальный раздел `led` в настройках каждого контроллера:
|
||||||
|
```yaml hl_lines="3"
|
||||||
|
mega:
|
||||||
|
megaid1:
|
||||||
|
led:
|
||||||
|
ledid1: # id, который вы придумываете сами
|
||||||
|
# конфиг
|
||||||
|
```
|
||||||
|
Далее интеграция имеет поддержку двух типов лент
|
||||||
|
#### На диммерах
|
||||||
|
Интеграция может превратить любые 3 (или 4) диммера (актуально для мосфетов на pwm-расширителе или моноблоке)
|
||||||
|
в rgb(w) контроллер с интерфейсом выбора цвета. Конфиг для такого типа ленты будет выглядеть так:
|
||||||
|
!!! note ""
|
||||||
|
- **ports** (list\[str\]): список номеров портов, из которых составлять ленту, все порты должны быть типа PWM.
|
||||||
|
Порядок цветов строго \[R, G, B, W\]. W указывается опционально
|
||||||
|
- **white_sep** (bool, true): яркостью белого можно управлять в двух режимах - синхронно с яркостью RGB, либо
|
||||||
|
отдельно, по умолчанию стоит отдельно (true)
|
||||||
|
- **smooth** (float, 1): скорость диммирования, от 0 до 100% за <smooth> секунд.
|
||||||
|
Пример:
|
||||||
|
```yaml
|
||||||
|
some_led1:
|
||||||
|
ports: [10, 12, 15, 16]
|
||||||
|
white_sep: true
|
||||||
|
smooth: 2
|
||||||
|
```
|
||||||
|
#### Адресные ленты на WS281X
|
||||||
|
Подробно про поддержку контроллера таких лент рассказано в [инструкции](https://www.ab-log.ru/smart-house/ethernet/megad-2561#ws).
|
||||||
|
Интеграция не привносит ничего нового - только помогает "пробросить" такие ленты в интерфейс HA.
|
||||||
|
Конфиг таких лент выглядит так:
|
||||||
|
!!! note ""
|
||||||
|
- **ws28xx** (bool, false): обязательное поле, необходимо установить true.
|
||||||
|
- **port** (int): номер порта, на котором настроена лента.
|
||||||
|
- **order** (str, "rgb"): последовательность каналов, допускаются любые комбинации букв r,g,b: rbg, bgr и тд
|
||||||
|
- **chip** (int, 100): кол-во чипов в ленте, по умолчанию 100, если их меньше емеет смысл указать правильное кол-во
|
||||||
|
для увеличения скорости плавного диммирования
|
||||||
|
- **smooth** (float, 1): скорость диммирования, от 0 до 100% за <smooth> секунд.
|
||||||
|
Пример:
|
||||||
|
```yaml
|
||||||
|
some_led2:
|
||||||
|
ws28xx: true
|
||||||
|
port: 36
|
||||||
|
order: bgr
|
||||||
|
smooth: 2
|
||||||
|
```
|
||||||
|
### Бинарные сенсоры {: #binary}
|
||||||
|
Или по-другому цифровые входы. Как правило используются для выключателей, кнопок, датчиков движения и тд.
|
||||||
|
!!! note ""
|
||||||
|
- **response_template** (str): шаблон ответа на команды сервера. По-умолчанию d.
|
||||||
|
Про формат ответа подробно описано [тут](https://www.ab-log.ru/smart-house/ethernet/megad-2561#conf-in-act).
|
||||||
|
В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id).
|
||||||
|
Про отладку шаблонов подробнее [тут](http.md#temp-debug)
|
||||||
|
|
||||||
|
!!! note "на меге"
|
||||||
|
Для корректной работы binary_sensor имеет смысл использовать режим P&R в настройках порта меги
|
||||||
|
Бинарные сенсоры так же отвечают за события типа *mega.binary*, [об этом подробнее в разделе события](events.md)
|
||||||
|
|
||||||
|
Примеры шаблонов ответа:
|
||||||
|
```yaml
|
||||||
|
4:
|
||||||
|
response_template: "5:2" # простейший пример без шаблона. Каждый раз когда будет приходить сообщение на этот порт,
|
||||||
|
# будем менять состояние на противоположное
|
||||||
|
5:
|
||||||
|
# пример с использованием шаблона, порт 1 будет выключен если он сейчас включен и включен с последней сохраненной
|
||||||
|
# яркостью если он сейчас выключен
|
||||||
|
response_template: >-
|
||||||
|
{% if is_state('light.some_port_1', 'on') %}
|
||||||
|
1:0
|
||||||
|
{% else %}
|
||||||
|
1:{{state_attr('light.some_port_1', 'brightness')}}
|
||||||
|
{% endif %}
|
||||||
|
6:
|
||||||
|
# в шаблон так же передаются все параметры, которые передает контроллер (pt, cnt, m, click)
|
||||||
|
# эти параметры можно использовать в условиях или непосредственно в шаблоне в виде {{pt}}
|
||||||
|
response_template: >-
|
||||||
|
{% if m==2 %}1:0{% else %}d{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Датчики {: #sensors }
|
||||||
|
Любой датчик будь то i2c или аналоговый или 1-wire
|
||||||
|
!!! note ""
|
||||||
|
- **unit_of_measurement** (str): единицы измерения, [список доступных](https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes)
|
||||||
|
- **value_template** (str): шаблон для конвертации, например `{{(value|float)/100}}`
|
||||||
|
- **device_class** (str): класс устройства, [список доступных](https://developers.home-assistant.io/docs/core/entity/sensor#available-device-classes)
|
||||||
|
- **hex_to_float** (bool, false): если ваш датчик возвращает float запакованный в HEX, интеграция его распакует (перед применением темплейта)
|
||||||
|
- **filter_high** (float, none): верхняя граница значений, выше нее значения будут считаться ошибочными и отбрасываться. [Доступно так же глобальное значение](#filter_high)
|
||||||
|
- **filter_low** (float, none): нижняя граница значений, ниже нее значения будут считаться ошибочными и отбрасываться. [Доступно так же глобальное значение](#filter_low)
|
||||||
|
- **filter_values** ([float], none): список значений, которые считаются ошибочными. [Доступно так же глобальное значение](#filter_values)
|
||||||
|
- **filter_scale** (float, none): значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если
|
||||||
|
установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться.
|
||||||
|
[Доступно так же глобальное значение](#filter_scale)
|
||||||
|
- **fill_na** (str, last): чем заполнять пропуски, по-умолчанию last, что означает последнее значение, можно так же поставить none-тогда будут пропуски (разрывы на графике).
|
||||||
|
|
||||||
|
При этом есть так же особенности адресации, так для сенсора на одном порте с одним значением:
|
||||||
|
```yaml
|
||||||
|
36:
|
||||||
|
# конфиг
|
||||||
|
```
|
||||||
|
#### DHT11/22
|
||||||
|
На этих сенсорах два значения, одно для температуры, второе для влажности, поэтому для них применяется
|
||||||
|
особый вид адресации:
|
||||||
|
```yaml
|
||||||
|
35:
|
||||||
|
name:
|
||||||
|
hum: "влажность"
|
||||||
|
temp: "температура"
|
||||||
|
# и так далее для любого параметра сенсоров
|
||||||
|
```
|
||||||
|
Логика так себе ) Но так повелось в первых версиях.
|
||||||
|
#### 1W-BUS
|
||||||
|
Для датчиков установленных в шину 1-wire адресация кастомизации такая:
|
||||||
|
```yaml
|
||||||
|
35:
|
||||||
|
addr: # адрес датчика, по-умолчанию entity_id будет состоять из адреса и типа
|
||||||
|
# конфиг
|
||||||
|
```
|
||||||
|
#### i2c {: #i2c}
|
||||||
|
Для сенсоров i2c нужно так же указать id сенсора, который можно посмотреть в атрибутах объекта на [странице разработчика](https://my.home-assistant.io/redirect/developer_states/).
|
||||||
|
```yaml
|
||||||
|
36:
|
||||||
|
htu21d_humidity: # i2c_id
|
||||||
|
# конфиг
|
||||||
|
```
|
||||||
|
[Подробнее про i2c](i2c.md)
|
||||||
|
## Параметры контроллера
|
||||||
|
Некоторые параметры применяются для всего контроллера (одна мега)
|
||||||
|
### def_response
|
||||||
|
Шаблон ответа сервера по умолчанию. Если этот параметр указан, то настройка "d по умолчанию"
|
||||||
|
в UI игнорируется.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
```yaml
|
||||||
|
mega:
|
||||||
|
megaid1:
|
||||||
|
def_response: >-
|
||||||
|
{% if m in [0, 1] %}d{% endif %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Параметры интеграции
|
||||||
|
### allow_hosts {: #allow_hosts }
|
||||||
|
Отвечает за список хостов, с которых интеграция "слушает" сообщения. По умолчанию, в этот список
|
||||||
|
входят все настроенные меги, а так же все запросы с локального хоста.
|
||||||
|
|
||||||
|
Иногда, в целях отладки, требуется расширить этот список, что можно сделать следующим оьразом:
|
||||||
|
```yaml
|
||||||
|
mega:
|
||||||
|
allow_hosts:
|
||||||
|
- 192.168.1.20
|
||||||
|
```
|
||||||
|
|
||||||
|
### filter_high {: #filter_high}
|
||||||
|
Верхняя граница значений датчиков по-умолчанию, выше нее значения будут считаться ошибочными и отбрасываться
|
||||||
|
|
||||||
|
### filter_low {: #filter_low}
|
||||||
|
Нижняя граница значений датчиков по-умолчанию, ниже нее значения будут считаться ошибочными и отбрасываться
|
||||||
|
|
||||||
|
### filter_values {: #filter_values }
|
||||||
|
Список значений, которые считаются ошибочными, настройка по-умолчанию для всех датчиков. Удобно, если у вас много
|
||||||
|
однотипных датчиков
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
mega:
|
||||||
|
filter_values: [-82, - 150]
|
||||||
|
```
|
||||||
|
### filter_scale {: #filter_scale }
|
||||||
|
Значение отклонения от текущего значения, которое будет считаться выбросом и отфильтруется, например если
|
||||||
|
установить 1, то это означает, что при росте показателя сенсора на 100% и больше или падении на 100% и больше, такое значение не будет отображаться.
|
||||||
|
```yaml
|
||||||
|
mega:
|
||||||
|
filter_scale: 1 # 100%
|
||||||
|
```
|
||||||
34
mkdocs.yml
Normal file
34
mkdocs.yml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
site_name: MegaD HomeAssistant integration
|
||||||
|
repo_url: https://github.com/andvikt/mega_hacs
|
||||||
|
site_url: http://127.0.0.1:8000
|
||||||
|
extra:
|
||||||
|
disqus: mega-hacs
|
||||||
|
analytics:
|
||||||
|
provider: google
|
||||||
|
property: G-E3LX9D6959
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
markdown_extensions:
|
||||||
|
- toc:
|
||||||
|
permalink: "#"
|
||||||
|
- attr_list:
|
||||||
|
- pymdownx.highlight
|
||||||
|
- pymdownx.superfences
|
||||||
|
- admonition
|
||||||
|
plugins:
|
||||||
|
- search
|
||||||
|
nav:
|
||||||
|
- Главное: index.md
|
||||||
|
- Конфигурация:
|
||||||
|
- В интерфейсе: settings.md
|
||||||
|
- Настройка обратной связи: http.md
|
||||||
|
- Кастомизация: yaml.md
|
||||||
|
- i2c: i2c.md
|
||||||
|
- Плавные переходы: smooth.md
|
||||||
|
- Автоматизация:
|
||||||
|
- Шаблоны (Blueprints): blueprints.md
|
||||||
|
- События: events.md
|
||||||
|
- Сервисы: services.md
|
||||||
|
- Неполадки: debug.md
|
||||||
|
extra_javascript:
|
||||||
|
- "https://buttons.github.io/buttons.js"
|
||||||
69
readme.md
69
readme.md
@@ -9,71 +9,4 @@
|
|||||||
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
|
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
|
||||||
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
|
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
|
||||||
|
|
||||||
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
|
[Подробная документация](https://andvikt.github.io/mega_hacs/)
|
||||||
|
|
||||||
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
|
|
||||||
|
|
||||||
Предложения по доработкам просьба писать в [discussions](https://github.com/andvikt/mega_hacs/discussions), о проблемах
|
|
||||||
создавать [issue](https://github.com/andvikt/mega_hacs/issues/new/choose)
|
|
||||||
## Основные особенности:
|
|
||||||
- Настройка в веб-интерфейсе + [yaml](https://github.com/andvikt/mega_hacs/wiki/Кастомизация)
|
|
||||||
- Все порты автоматически добавляются как устройства (для обычных релейных выходов создается
|
|
||||||
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для датчиков
|
|
||||||
`sensor`)
|
|
||||||
- Возможность работы с несколькими megad
|
|
||||||
- Автоматическое восстановление состояний выходов после перезагрузки контроллера
|
|
||||||
- Обратная связь по [http](https://github.com/andvikt/mega_hacs/wiki/http) или mqtt (`deprecated`, поддержка mqtt
|
|
||||||
будет выключена в версиях >= 1.0.0, тк в нем нет необходимости)
|
|
||||||
- [События](https://github.com/andvikt/mega_hacs/wiki/События) на двойные/долгие нажатия
|
|
||||||
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad, это дает гарантии надежного исполнения
|
|
||||||
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
|
|
||||||
выполнении предыдущей.
|
|
||||||
- поддержка [ds2413](https://www.ab-log.ru/smart-house/ethernet/megad-2w) (начиная с версии 0.4.1)
|
|
||||||
- поддержка MCP23008/MCP23017/PCA9685 (начиная с версии 0.5.1)
|
|
||||||
- поддержка всех возможных датчиков в режиме I2C-ANY, полный список поддерживаемых датчиков
|
|
||||||
[по ссылке](https://github.com/andvikt/mega_hacs/wiki/i2c) (начиная с версии 0.6.1)
|
|
||||||
|
|
||||||
## Установка
|
|
||||||
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
|
|
||||||
|
|
||||||
HACS - Integrations - Explore, в поиске ищем MegaD.
|
|
||||||
|
|
||||||
Чтобы включить возможность использования бета-версий, зайдите в HACS, найдите интеграцию MegaD, нажмите три точки,
|
|
||||||
там кнопка "переустановить" или reinstall, дальше нужно нажать галку "показывать бета-версии"
|
|
||||||
|
|
||||||
Обновления выполняются так же в меню HACS.
|
|
||||||
Информация об обновлениях приходит с некоторым интервалом, чтобы вручную проверить наличие обновлений
|
|
||||||
нажмите три точки возле интеграции в меню HACS и нажмите `обновить информацию`
|
|
||||||
|
|
||||||
Альтернативный способ установки:
|
|
||||||
```shell
|
|
||||||
# из папки с конфигом
|
|
||||||
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
|
|
||||||
```
|
|
||||||
Не забываем перезагрузить HA
|
|
||||||
|
|
||||||
## Настройка
|
|
||||||
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
|
|
||||||
|
|
||||||
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
|
|
||||||
|
|
||||||
В самой меге необходимо прописать настройки:
|
|
||||||
```yaml
|
|
||||||
srv: "192.168.1.4:8123" # ip:port вашего HA
|
|
||||||
script: "mega" # это api интеграции, к которому будет обращаться контроллер
|
|
||||||
```
|
|
||||||
Так же необходимо настроить Mega-ID в настройках контроллера, для каждой меги id должен быть разным.
|
|
||||||
|
|
||||||
При любых изменениях настроек контроллера (типы входов, id и тд) необходимо в настройках интеграции нажать `Обновить
|
|
||||||
объекты`
|
|
||||||
|
|
||||||
## Зависимости
|
|
||||||
Для максимальной скорости реакции на команды сервера, рекомендуется выключить `имитацию http-ответа` в
|
|
||||||
настройках интеграции и настроить proxy_pass к HA, самый простой способ сделать это - воспользоваться
|
|
||||||
[специальным аддоном](https://github.com/andvikt/mega_addon/tree/master/mega-proxy)
|
|
||||||
|
|
||||||
Обновить ваш контроллер до последней версии, обновление прошивки MegaD можно делать
|
|
||||||
из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
|
|
||||||
|
|
||||||
|
|
||||||
Подробная документация по [ссылке](https://github.com/andvikt/mega_hacs/wiki)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user