Compare commits

...

16 Commits

Author SHA1 Message Date
Andrey
c0b1247b9e smaller headers 2021-01-25 21:35:06 +03:00
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
10 changed files with 154 additions and 73 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

@@ -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

@@ -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

@@ -8,10 +8,10 @@ from aiohttp.web_request import Request
from aiohttp.web_response import Response from aiohttp.web_response import Response
from homeassistant.helpers.template import Template from homeassistant.helpers.template import Template
from .const import EVENT_BINARY_SENSOR, CONF_HTTP, DOMAIN, CONF_CUSTOM, CONF_RESPONSE_TEMPLATE from .const import EVENT_BINARY_SENSOR, DOMAIN, CONF_RESPONSE_TEMPLATE
from homeassistant.components.http import HomeAssistantView from homeassistant.components.http import HomeAssistantView
from homeassistant.core import callback, HomeAssistant from homeassistant.core import HomeAssistant
from . import hub from .tools import make_ints
_LOGGER = logging.getLogger(__name__).getChild('http') _LOGGER = logging.getLogger(__name__).getChild('http')
@@ -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]
@@ -62,9 +61,10 @@ class MegaView(HomeAssistantView):
make_ints(data) make_ints(data)
port = data.get('pt') port = data.get('pt')
data = data.copy() data = data.copy()
data['mega_id'] = hub.id
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:
@@ -73,8 +73,7 @@ class MegaView(HomeAssistantView):
template.hass = hass template.hass = hass
ret = template.async_render(data) ret = template.async_render(data)
_LOGGER.debug('response %s', ret) _LOGGER.debug('response %s', ret)
ret = Response(body=ret or 'd', content_type='text/plain', headers={}) ret = Response(body=ret or 'd', content_type='text/plain', headers={'Server': 's', 'Date': 'n'})
ret.headers.clear()
return ret return ret
async def later_update(self, hub): async def later_update(self, hub):
@@ -83,13 +82,3 @@ class MegaView(HomeAssistantView):
await hub.updater.async_refresh() await hub.updater.async_refresh()
def make_ints(d: dict):
for x in d:
try:
d[x] = float(d[x])
except ValueError:
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0

View File

@@ -14,9 +14,10 @@ from homeassistant.const import DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import TEMP, HUM, PATT_SPLIT, DOMAIN, CONF_HTTP from .const import TEMP, HUM, PATT_SPLIT, DOMAIN, CONF_HTTP, EVENT_BINARY_SENSOR
from .exceptions import CannotConnect, MqttNotConfigured from .exceptions import CannotConnect, MqttNotConfigured
from .http import MegaView from .http import MegaView
from .tools import make_ints
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\.]+)')
@@ -60,8 +61,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 +125,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 +165,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 +197,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 +220,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
@@ -275,9 +291,16 @@ class MegaD:
value = None value = None
try: try:
value = json.loads(msg.payload) value = json.loads(msg.payload)
value = make_ints(value)
self.values[port] = value self.values[port] = value
for cb in self._callbacks[port]: for cb in self._callbacks[port]:
cb(value) cb(value)
value = value.copy()
value['mega_id'] = self.id
self.hass.bus.async_fire(
EVENT_BINARY_SENSOR,
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
@@ -292,7 +315,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 +369,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 +380,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 +409,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

@@ -0,0 +1,10 @@
def make_ints(d: dict):
for x in d:
try:
d[x] = float(d[x])
except ValueError:
pass
if 'm' not in d:
d['m'] = 0
if 'click' not in d:
d['click'] = 0

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

@@ -26,8 +26,40 @@ wget -q -O - https://raw.githubusercontent.com/andvikt/mega_hacs/master/install.
``` ```
Не забываем перезагрузить HA Не забываем перезагрузить 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
@@ -60,7 +92,7 @@ script: "mega" # это api интеграции, к которому будет
запустить то что прописано в поле act в настройках порта). запустить то что прописано в поле act в настройках порта).
Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не Поддерживаеются шаблоны HA. Это может быть использовано, например, для запоминания яркости (тк сам контроллер этого не
умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, value) умеет). В шаблоне можно использовать параметры, которые передает контроллер (m, click, pt, mdid, mega_id)
Примеры: Примеры:
```yaml ```yaml
@@ -102,37 +134,7 @@ mega:
curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1' curl -v -X GET 'http://192.168.88.1.4:8123/mega?pt=5&m=1'
``` ```
В ответ будет приходить либо `d`, либо скрипт, который вы настроили В ответ будет приходить либо `d`, либо скрипт, который вы настроили
## Настройка
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем 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" # если датчик один, то просто строчкой
```
## События ## События
`binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать `binary_sensor` срабатывает когда цифровой выход принимает значение 'ON'. `binary_sensor` имеет смысл использовать
@@ -152,6 +154,15 @@ mega:
- service: light.toggle - service: light.toggle
entity_id: light.some_light entity_id: light.some_light
``` ```
События могут содержать следующие поля:
- mega_id: id как в конфиге HA
- pt: номер порта
- cnt: счетчик срабатываний
- mdid: if как в конфиге контроллера
- click: клик (подробнее в документации меги)
- value: текущее значение (только для mqtt)
- port: номер порта
Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться Чтобы понять, какие события происходят, лучше всего воспользоваться панелью разработчика и подписаться
на вкладке события на событие `mega.sensor`, понажимать кнопки. на вкладке события на событие `mega.sensor`, понажимать кнопки.