Compare commits

...

16 Commits

Author SHA1 Message Date
Andrey
1548e8c364 fix multiple megas 2021-01-25 20:17:15 +03:00
Andrey
39c4ab0e3b fix device name 2021-01-25 18:45:23 +03:00
Andrey
a002e48e04 fix old mega out type 0 2021-01-25 18:13:25 +03:00
Andrey
dc6bdfc8f4 fix yaml exclusion 2021-01-25 17:56:47 +03:00
Andrey
e51b50797c fix yaml exclusion 2021-01-25 17:46:26 +03:00
Andrey
c4205c7ddc fix sw-link 2021-01-25 17:23:33 +03:00
Andrey
6164966d0b fix scanning 2021-01-25 17:21:55 +03:00
Andrey
ff6225a959 fix scanning 2021-01-25 17:18:42 +03:00
Andrey
57f355d479 fix scanning 2021-01-25 16:37:48 +03:00
Andrey
7d6273539e fix scanning 2021-01-25 15:57:10 +03:00
Andrey
5da2973351 fix scanning 2021-01-25 15:52:24 +03:00
Andrey
5eadd295f1 Merge branch 'master' of https://github.com/andvikt/mega_hacs 2021-01-25 15:48:29 +03:00
Andrey
5bf432a27f change readme 2021-01-25 15:47:55 +03:00
Andrey
d934e87ae5 force use http while scanning 2021-01-25 15:46:40 +03:00
andvikt
0b54db9c44 Update issue templates 2021-01-25 13:05:31 +03:00
Andrey
2d15b60929 add allowed hosts config 2021-01-25 12:54:15 +03:00
11 changed files with 149 additions and 66 deletions

26
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@@ -0,0 +1,26 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Описание**
A clear and concise description of what the bug is.
**Версии систем**
Enviroment: raspberry/linux/windows/macos/docker
HA version:
mega_hacs version:
megad firmware version:
**Ожидаемое поведение**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**LOG**
Прочитайте в документации как включить подробный лог интеграции и приложите его здесь

View File

@@ -16,7 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.components import mqtt 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 DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF_CUSTOM, CONF_SKIP, CONF_PORT_TO_SCAN, \
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS
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
@@ -26,6 +26,7 @@ _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: { DOMAIN: {
vol.Optional(CONF_ALLOW_HOSTS): [str],
vol.Required(str, description='id меги из веб-интерфейса'): { vol.Required(str, description='id меги из веб-интерфейса'): {
vol.Optional(int, description='номер порта'): { vol.Optional(int, description='номер порта'): {
vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool, vol.Optional(CONF_SKIP, description='исключить порт из сканирования', default=False): bool,
@@ -62,6 +63,7 @@ async def async_setup(hass: HomeAssistant, config: dict):
"""YAML-конфигурация содержит только кастомизации портов""" """YAML-конфигурация содержит только кастомизации портов"""
hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})} hass.data[DOMAIN] = {CONF_CUSTOM: config.get(DOMAIN, {})}
hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(DOMAIN, {})) hass.data[DOMAIN][CONF_HTTP] = view = MegaView(cfg=config.get(DOMAIN, {}))
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, 'save', partial(_save_service, hass), schema=vol.Schema({

View File

@@ -25,15 +25,15 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
vol.Optional(CONF_PORT_TO_SCAN, default=0): int, vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
vol.Optional(CONF_MQTT_INPUTS, default=True): bool, vol.Optional(CONF_MQTT_INPUTS, default=True): 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,
}, },
) )
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, mqtt=_mqtt, loop=asyncio.get_event_loop())
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():

View File

@@ -23,6 +23,7 @@ CONF_RESPONSE_TEMPLATE = 'response_template'
CONF_ACTION = 'action' CONF_ACTION = 'action'
CONF_UPDATE_ALL = 'update_all' CONF_UPDATE_ALL = 'update_all'
CONF_GET_VALUE = 'get_value' CONF_GET_VALUE = 'get_value'
CONF_ALLOW_HOSTS = 'allow_hosts'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",

View File

@@ -59,7 +59,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
"config_entries": [ "config_entries": [
self.config_entry, self.config_entry,
], ],
"name": f'port {self.port}', "name": f'{self._mega_id} port {self.port}',
"manufacturer": 'ab-log.ru', "manufacturer": 'ab-log.ru',
# "model": self.light.productname, # "model": self.light.productname,
# "sw_version": self.light.swversion, # "sw_version": self.light.swversion,

View File

@@ -26,8 +26,7 @@ class MegaView(HomeAssistantView):
def __init__(self, cfg: dict): def __init__(self, cfg: dict):
self._try = 0 self._try = 0
self.allowed_hosts = {'::1'} self.allowed_hosts = {'::1'}
self.callbacks: typing.DefaultDict[int, typing.List[typing.Callable[[dict], typing.Coroutine]]] \ self.callbacks = defaultdict(lambda: defaultdict(list))
= defaultdict(list)
self.templates: typing.Dict[str, typing.Dict[str, Template]] = { self.templates: typing.Dict[str, typing.Dict[str, Template]] = {
mid: { mid: {
pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE] pt: cfg[mid][pt][CONF_RESPONSE_TEMPLATE]
@@ -64,7 +63,7 @@ class MegaView(HomeAssistantView):
data = data.copy() data = data.copy()
ret = 'd' ret = 'd'
if port is not None: if port is not None:
for cb in self.callbacks[port]: for cb in self.callbacks[hub.id][port]:
cb(data) cb(data)
template: Template = self.templates.get(hub.id, {}).get(port) template: Template = self.templates.get(hub.id, {}).get(port)
if hub.update_all: if hub.update_all:

View File

@@ -60,8 +60,9 @@ class MegaD:
): ):
"""Initialize.""" """Initialize."""
if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False: if mqtt_inputs is None or mqtt_inputs == 'None' or mqtt_inputs is False:
self.http = hass.data[DOMAIN][CONF_HTTP] self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
self.http.allowed_hosts |= {host} if not self.http is None:
self.http.allowed_hosts |= {host}
else: else:
self.http = None self.http = None
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
@@ -123,10 +124,16 @@ class MegaD:
async with self.lck: async with self.lck:
self.entities.append(ent) self.entities.append(ent)
async def get_sensors(self): async def get_sensors(self, only_list=False):
self.lg.debug(self.sensors) self.lg.debug(self.sensors)
ports = []
for x in self.sensors: for x in self.sensors:
await self.get_port(x) if only_list and x.http_cmd != 'list':
continue
if x.port in ports:
continue
await self.get_port(x.port, force_http=True, http_cmd=x.http_cmd)
ports.append(x.port)
@property @property
def is_online(self): def is_online(self):
@@ -157,6 +164,7 @@ class MegaD:
self.lg.debug('poll') self.lg.debug('poll')
if self.mqtt is None: if self.mqtt is None:
await self.get_all_ports() await self.get_all_ports()
await self.get_sensors(only_list=True)
return return
if len(self.sensors) > 0: if len(self.sensors) > 0:
await self.get_sensors() await self.get_sensors()
@@ -188,7 +196,9 @@ class MegaD:
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text()) self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
return None return None
else: else:
return await req.text() ret = await req.text()
self.lg.debug('response %s', ret)
return ret
async def save(self): async def save(self):
await self.send_command(cmd='s') await self.send_command(cmd='s')
@@ -209,15 +219,20 @@ class MegaD:
ret = {'value': ret} ret = {'value': ret}
return ret return ret
async def get_port(self, port): async def get_port(self, port, force_http=False, http_cmd='get'):
""" """
Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное Запрос состояния порта. Состояние всегда возвращается в виде объекта, всегда сохраняется в центральное
хранилище values хранилище values
""" """
self.lg.debug(f'get port %s', port) self.lg.debug(f'get port %s', port)
if self.mqtt is None: if self.mqtt is None or force_http:
ret = await self.request(pt=port, cmd='get') ret = await self.request(pt=port, cmd=http_cmd)
ret = self.parse_response(ret) ret = self.parse_response(ret)
self.lg.debug('parsed: %s', ret)
if http_cmd == 'list' and isinstance(ret, dict) and 'value' in ret:
await asyncio.sleep(1)
ret = await self.request(pt=port, http_cmd=http_cmd)
ret = self.parse_response(ret)
self.values[port] = ret self.values[port] = ret
return ret return ret
@@ -292,7 +307,7 @@ class MegaD:
if self.mqtt_inputs: if self.mqtt_inputs:
self._callbacks[port].append(callback) self._callbacks[port].append(callback)
else: else:
self.http.callbacks[port].append(callback) 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."""
@@ -346,7 +361,7 @@ class MegaD:
return pty, m return pty, m
async def scan_ports(self, nports=37): async def scan_ports(self, nports=37):
for x in range(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]
@@ -357,11 +372,15 @@ class MegaD:
async for port, pty, m in self.scan_ports(nports): async for port, pty, m in self.scan_ports(nports):
if pty == "0": if pty == "0":
ret['binary_sensor'][port].append({}) ret['binary_sensor'][port].append({})
elif pty == "1" and m in ['0', '1']: elif pty == "1" and (m in ['0', '1', '3'] or m is None):
ret['light'][port].append({'dimmer': m == '1'}) ret['light'][port].append({'dimmer': m == '1'})
elif pty == '3': elif pty == '3':
try: try:
values = await self.get_port(port) http_cmd = 'get'
values = await self.get_port(port, force_http=True)
if values is None or (isinstance(values, dict) and str(values.get('value')) in ('', 'None')):
values = await self.get_port(port, force_http=True, http_cmd='list')
http_cmd = 'list'
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.lg.warning(f'timout on port {port}') self.lg.warning(f'timout on port {port}')
continue continue
@@ -382,6 +401,7 @@ class MegaD:
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), unit_of_measurement=UNITS.get(key, UNITS[TEMP]),
device_class=CLASSES.get(key, CLASSES[TEMP]), device_class=CLASSES.get(key, CLASSES[TEMP]),
id_suffix=key, id_suffix=key,
http_cmd=http_cmd,
)) ))
return ret return ret

View File

@@ -101,6 +101,7 @@ class Mega1WSensor(MegaPushEntity):
unit_of_measurement, unit_of_measurement,
device_class, device_class,
key=None, key=None,
http_cmd='get',
*args, *args,
**kwargs **kwargs
): ):
@@ -115,8 +116,8 @@ class Mega1WSensor(MegaPushEntity):
self.key = key self.key = key
self._device_class = device_class self._device_class = device_class
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
if self.port not in self.mega.sensors: self.mega.sensors.append(self)
self.mega.sensors.append(self.port) self.http_cmd = http_cmd
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
@@ -143,16 +144,30 @@ class Mega1WSensor(MegaPushEntity):
@property @property
def state(self): def state(self):
ret = None
if self.key: if self.key:
ret = self.mega.values.get(self.port, {}).get('value', {}).get(self.key) try:
ret = self.mega.values.get(self.port, {})
if isinstance(ret, dict):
ret = ret.get(self.key)
except:
self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
return
else: else:
ret = self.mega.values.get(self.port, {}).get('value') ret = self.mega.values.get(self.port, {}).get('value')
if ret is None and self._state is not None: if ret is None and self._state is not None:
ret = self._state.state ret = self._state.state
try:
ret = float(ret)
ret = str(ret)
except:
ret = None
return ret return ret
@property @property
def name(self): def name(self):
n = super().name n = super().name
c = self.customize.get(CONF_NAME, {}).get(self.key) c = self.customize.get(CONF_NAME, {})
if isinstance(c, dict):
c = c.get(self.key)
return c or n return c or n

View File

@@ -37,7 +37,8 @@
"reload": "[%key:common::config_flow::data::reload%]", "reload": "[%key:common::config_flow::data::reload%]",
"invert": "[%key:common::config_flow::data::invert%]", "invert": "[%key:common::config_flow::data::invert%]",
"mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]", "mqtt_inputs": "[%key:common::config_flow::data::mqtt_inputs%]",
"nports": "[%key:common::config_flow::data::nports%]" "nports": "[%key:common::config_flow::data::nports%]",
"update_all": "[%key:common::config_flow::data::update_all%]"
} }
} }
} }

View File

@@ -21,7 +21,8 @@
"scan_interval": "Scan interval (sec), 0 - don't update", "scan_interval": "Scan interval (sec), 0 - don't update",
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)", "port_to_scan": "Port to poll aliveness (needed only if no sensors used)",
"nports": "Number of ports", "nports": "Number of ports",
"update_all": "Update all outs when input" "update_all": "Update all outs when input",
"mqtt_inputs": "Use MQTT"
} }
} }
} }

View File

@@ -14,8 +14,52 @@
большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о большого кол-ва команд (например в сценах). Каждая следующая команда отправляется только после получения ответа о
выполнении предыдущей. выполнении предыдущей.
## Установка
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation):
HACS - Integrations - Explore, в поиске ищем MegaD.
Альтернативный способ установки:
```shell
# из папки с конфигом
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
```
Не забываем перезагрузить HA
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml
# configuration.yaml
mega:
hello: # ID меги, как в UI
7: # номер порта
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
14:
name: какой-то датчик
unit_of_measurement: "°C" # если датчик один, то просто строчкой
```
## Зависимости ## Зависимости
**Важно!!** Для максимальной совместимости необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/) Для совместимости c mqtt необходимо настроить интеграцию [mqtt](https://www.home-assistant.io/integrations/mqtt/)
в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt в HomeAssistant, а так же обновить ваш контроллер до последней версии, тк были важные обновления в части mqtt
## HTTP in ## HTTP in
@@ -73,50 +117,24 @@ mega:
{% if m==2 %}1:0{% else %}d{% endif %} {% if m==2 %}1:0{% else %}d{% endif %}
``` ```
## Отладка ответов
## Установка Для отладки ответов сервера можно самим имитировать запросы контроллера, если у вас есть доступ к консоли
Рекомендованный способ с поддержкой обновлений - [HACS](https://hacs.xyz/docs/installation/installation): HA:
HACS - Integrations - Explore, в поиске ищем MegaD.
Альтернативный способ установки:
```shell ```shell
# из папки с конфигом curl -v -X GET 'http://localhost:8123/mega?pt=5&m=1'
wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.sh | bash -
``` ```
Не забываем перезагрузить HA Если доступа нет, нужно в файл конфигурации добавить ip компьюетра, с которого вы хотите делать запросы, например:
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
Все имеющиеся у вас порты будут настроены автоматически. Вы можете менять названия, иконки и entity_id так же из интерфейса.
#### Кастомизация устройств с помощью yaml:
```yaml ```yaml
# configuration.yaml
mega: mega:
hello: # ID меги, как в UI allow_hosts:
7: # номер порта - 192.168.1.1
domain: switch # тип устройства (switch или light, по умолчанию для цифровых выходов используется light)
invert: true # инвертировать или нет (по умолчанию false)
name: Насос # имя устройства
8:
# исключить из сканирования
skip: true
33:
# для датчиков можно кастомизировать только имя и unit_of_measurement
# для температуры и влажность unit определяется автоматически, для остальных юнита нет
name:
hum: "влажность"
temp: "температура"
unit_of_measurement:
hum: "%" # если датчиков несколько, то можно указывать юниты по их ключам
temp: "°C"
14:
name: какой-то датчик
unit_of_measurement: "°C" # если датчик один, то просто строчкой
``` ```
И тогда можно с локальной машины делать запросы на ваш сервер HA:
```shell
curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1'
```
В ответ будет приходить либо `d`, либо скрипт, который вы настроили
## События ## События
`binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать `binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать