Compare commits

...

58 Commits

Author SHA1 Message Date
andvikt
5bb2425918 Merge pull request #158 from andvikt/dev
Dev
2023-10-10 23:03:24 +03:00
Викторов Андрей Германович
082c647110 Bump version: 1.1.8b0 → 1.1.8b1 2023-10-10 23:01:09 +03:00
Викторов Андрей Германович
21bc277b78 fix rgb order 2023-10-10 22:58:25 +03:00
andvikt
58a4680311 Merge pull request #157 from andvikt/dev
Dev
2023-10-10 21:05:03 +03:00
Викторов Андрей Германович
00e62ee83f Bump version: 1.1.7 → 1.1.8b0 2023-10-10 21:01:59 +03:00
Викторов Андрей Германович
b62406210c fix rgb color 2023-10-10 21:01:21 +03:00
andvikt
03462edf83 Merge pull request #156 from andvikt/dev
Dev
2023-10-09 14:59:50 +03:00
Викторов Андрей Германович
c45653b0ba Bump version: 1.1.7b0 → 1.1.7 2023-10-09 14:58:26 +03:00
Викторов Андрей Германович
683727b3ec Bump version: 1.1.6 → 1.1.7b0 2023-10-09 14:57:50 +03:00
Викторов Андрей Германович
2300d2750a . 2023-10-09 14:57:46 +03:00
Викторов Андрей Германович
ac8603f219 fix encoding issue 2023-10-09 14:55:15 +03:00
Викторов Андрей Германович
fbc1f31023 Bump version: 1.1.6b1 → 1.1.6 2023-08-21 08:55:54 +03:00
Викторов Андрей Германович
cd5ab3b689 Bump version: 1.1.6b0 → 1.1.6b1 2023-08-14 20:44:24 +03:00
Викторов Андрей Германович
0138f5e323 . 2023-08-14 20:43:26 +03:00
Викторов Андрей Германович
0ab4f6623c Bump version: 1.1.5 → 1.1.6b0 2023-08-14 20:38:08 +03:00
Викторов Андрей Германович
5664e1e929 . 2023-08-14 20:38:06 +03:00
Викторов Андрей Германович
bcf108fc9c . 2023-08-14 20:37:35 +03:00
Викторов Андрей Германович
ca3f90374b Bump version: 1.1.501 → 1.1.502b0 2023-08-14 20:37:08 +03:00
Викторов Андрей Германович
d15cce8061 Bump version: 1.1.5 → 1.1.501 2023-08-14 20:36:40 +03:00
Викторов Андрей Германович
4e83d81004 fix #152 2023-08-14 20:35:59 +03:00
Викторов Андрей Германович
9ceb544c1a Bump version: 1.1.5b0 → 1.1.5 2023-08-03 16:13:30 +03:00
Викторов Андрей Германович
c8b3cb60de Bump version: 1.1.4 → 1.1.5b0 2023-08-03 16:13:14 +03:00
Викторов Андрей Германович
bd550442d4 fix #151 2023-08-03 16:12:35 +03:00
andvikt
8903628b36 fix #120 2023-06-13 18:09:01 +03:00
andvikt
e1a6637f28 fix #145 2023-06-13 17:47:19 +03:00
andvikt
a346cb3fd7 fix #138 2023-06-13 17:39:49 +03:00
andvikt
d6ef137e75 fix #127 2023-06-13 17:15:42 +03:00
andvikt
8847290457 Merge pull request #124 from andvikt/dev
Dev
2022-09-08 13:28:01 +03:00
andvikt
d7180c0477 Bump version: 1.1.4b0 → 1.1.4 2022-09-08 12:42:10 +03:00
andvikt
1ceb7e4766 Bump version: 1.1.3 → 1.1.4b0 2022-09-08 12:41:56 +03:00
andvikt
ef46ac2b2b fix 2022.9 2022-09-08 12:41:42 +03:00
andvikt
36dfad1697 Merge pull request #113 from andvikt/dev
Dev
2022-07-22 10:17:28 +03:00
andvikt
dee0d6d1a7 Merge pull request #109 from wrt54g/hacs-url
Update HACS URL
2022-07-22 10:14:57 +03:00
andvikt
e44aef35fb Bump version: 1.1.3b0 → 1.1.3 2022-07-22 10:09:04 +03:00
andvikt
a860ba9822 Bump version: 1.1.2b0 → 1.1.3b0 2022-07-22 10:08:43 +03:00
andvikt
2463b270e5 fix asyncio.lock 2022-07-22 10:08:37 +03:00
Sven Serlier
da7305c07b Update HACS URL 2022-06-02 06:49:56 +02:00
andvikt
d3f3eeedc5 Update index.md 2022-02-11 21:43:58 +03:00
andvikt
ce2969c1e7 Bump version: 1.1.1 → 1.1.2b0 2021-12-19 17:52:57 +03:00
andvikt
34056273f3 try to fix sht31 2021-12-19 17:52:47 +03:00
andvikt
16ad1ea4d2 Merge pull request #97 from andvikt/dev
Dev
2021-12-15 16:58:42 +03:00
Викторов Андрей Германович
19313d3b35 Bump version: 1.1.1b1 → 1.1.1 2021-12-15 16:10:01 +03:00
Викторов Андрей Германович
cb14f9aa2a remove port in hub.binary_sesnors check when processing inbound message from controller 2021-12-15 16:08:06 +03:00
andvikt
b908068315 new config adressation 2021-12-01 18:49:37 +03:00
andvikt
c11de5c2b9 Bump version: 1.1.1b0 → 1.1.1b1 2021-11-07 12:38:18 +03:00
andvikt
679b53bbd3 new config adressation 2021-11-07 12:38:02 +03:00
Викторов Андрей Германович
2c2b3aab74 Bump version: 1.1.0 → 1.1.1b0 2021-11-03 10:24:29 +03:00
Викторов Андрей Германович
a77794dff7 fix sht31 updating issue
fix filters positive float
2021-11-03 10:24:21 +03:00
andvikt
ebb513b83d new config adressation 2021-10-29 21:14:50 +03:00
andvikt
f5519a595d Bump version: 1.1.0b0 → 1.1.0 2021-10-29 21:05:14 +03:00
andvikt
ee26759003 Bump version: 1.0.10b20 → 1.1.0b0 2021-10-29 21:04:30 +03:00
andvikt
8869ad9cee new config adressation 2021-10-29 20:14:18 +03:00
andvikt
27f1e05a3a new config adressation 2021-10-29 20:13:31 +03:00
andvikt
7fdc2fc9ff new config adressation 2021-10-29 20:12:59 +03:00
andvikt
28bf4393f0 Bump version: 1.0.10b19 → 1.0.10b20 2021-10-29 19:53:01 +03:00
andvikt
5ecb246eff fix brightness 2021-10-29 19:52:10 +03:00
Викторов Андрей Германович
3ee464a896 Bump version: 1.0.10b18 → 1.0.10b19 2021-10-29 15:26:28 +03:00
Викторов Андрей Германович
6368fd7cfc add fill_na 2021-10-29 15:26:21 +03:00
17 changed files with 585 additions and 437 deletions

View File

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

18
.gitignore vendored Normal file
View 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

View File

@@ -1,6 +1,7 @@
"""The mega integration.""" """The mega integration."""
import asyncio import asyncio
import logging import logging
import typing
from functools import partial from functools import partial
import voluptuous as vol import voluptuous as vol
@@ -17,7 +18,8 @@ from .const import DOMAIN, CONF_INVERT, CONF_RELOAD, PLATFORMS, CONF_PORTS, CONF
CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \ CONF_MQTT_INPUTS, CONF_HTTP, CONF_RESPONSE_TEMPLATE, CONF_ACTION, CONF_GET_VALUE, CONF_ALLOW_HOSTS, \
CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \ CONF_CONV_TEMPLATE, CONF_ALL, CONF_FORCE_D, CONF_DEF_RESPONSE, CONF_FORCE_I2C_SCAN, CONF_HEX_TO_FLOAT, \
RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE, \ RGB_COMBINATIONS, CONF_WS28XX, CONF_ORDER, CONF_SMOOTH, CONF_LED, CONF_WHITE_SEP, CONF_CHIP, CONF_RANGE, \
CONF_FILTER_VALUES, CONF_FILTER_SCALE, CONF_FILTER_LOW, CONF_FILTER_HIGH, CONF_FILL_NA 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
@@ -83,10 +85,10 @@ CUSTOMIZE_PORT = {
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_HEX_TO_FLOAT): bool,
vol.Optional(CONF_FILTER_VALUES): [cv.positive_float], vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)],
vol.Optional(CONF_FILTER_SCALE): cv.positive_float, vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float),
vol.Optional(CONF_FILTER_LOW): cv.positive_float, vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
vol.Optional(CONF_FILTER_HIGH): cv.positive_float, vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
vol.Optional(CONF_SMOOTH): cv.time_period_seconds, vol.Optional(CONF_SMOOTH): cv.time_period_seconds,
# vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений # vol.Optional(CONF_RANGE): vol.ExactSequence([int, int]), TODO: сделать отбрасывание "плохих" значений
vol.Optional(str): { vol.Optional(str): {
@@ -107,6 +109,11 @@ def extender(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(
{ {
@@ -129,11 +136,12 @@ CONFIG_SCHEMA = vol.Schema(
CUSTOMIZE_PORT, CUSTOMIZE_PORT,
CUSTOMIZE_DS2413, CUSTOMIZE_DS2413,
), ),
vol.Optional(CONF_FILTER_VALUES): [cv.positive_float], vol.Optional(CONF_FILTER_VALUES): [vol.Coerce(float)],
vol.Optional(CONF_FILTER_SCALE): cv.positive_float, vol.Optional(CONF_FILTER_SCALE): vol.Coerce(float),
vol.Optional(CONF_FILTER_LOW): cv.positive_float, vol.Optional(CONF_FILTER_LOW): vol.Coerce(float),
vol.Optional(CONF_FILTER_HIGH): cv.positive_float, vol.Optional(CONF_FILTER_HIGH): vol.Coerce(float),
} },
vol.Optional(CONF_1WBUS): [OWBUS]
} }
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,

View File

@@ -63,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 = 25 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):

View File

@@ -50,6 +50,8 @@ CONF_FILTER_VALUES = 'filter_values'
CONF_FILTER_SCALE = 'filter_scale' CONF_FILTER_SCALE = 'filter_scale'
CONF_FILTER_LOW = 'filter_low' CONF_FILTER_LOW = 'filter_low'
CONF_FILTER_HIGH = 'filter_high' CONF_FILTER_HIGH = 'filter_high'
CONF_1WBUS = '1wbus'
CONF_ADDR = 'addr'
PLATFORMS = [ PLATFORMS = [
"light", "light",
"switch", "switch",

View File

@@ -8,6 +8,7 @@ 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
@@ -86,7 +87,6 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
self.mega.ds2413_ports |= {self.port} self.mega.ds2413_ports |= {self.port}
super().__init__(coordinator=mega.updater) super().__init__(coordinator=mega.updater)
@property @property
def is_ws(self): def is_ws(self):
return False return False
@@ -130,11 +130,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
if self.hass is None or self.entity_id is None: if self.hass is None or self.entity_id is None:
return {} return {}
if self._customize is None: if self._customize is None:
c_entity_id = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM).get('entities', {}).get(self.entity_id, {}) c_entity_id = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM).get('entities', {}).get(self.entity_id, {})
self.lg.debug(
'customize %s with %s', self.entity_id, c_entity_id
)
c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {} c = self.hass.data.get(DOMAIN, {}).get(CONF_CUSTOM) or {}
c = c.get(self._mega_id) or {} c = c.get(self._mega_id) or {}
c = c.get(self.port) or {} c = c.get(self.port) or {}
@@ -147,7 +143,7 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
return self._customize return self._customize
@property @property
def device_info(self): def device_info(self) -> DeviceInfo:
if isinstance(self.port, list): if isinstance(self.port, list):
pt_idx = self.id_suffix pt_idx = self.id_suffix
else: else:
@@ -156,25 +152,19 @@ class BaseMegaEntity(CoordinatorEntity, RestoreEntity):
pt_idx, _ = _pt.split('e') pt_idx, _ = _pt.split('e')
else: else:
pt_idx = _pt pt_idx = _pt
return { return DeviceInfo(
"identifiers": { identifiers={
# Serial numbers are unique identifiers within a specific domain # Serial numbers are unique identifiers within a specific domain
(DOMAIN, f'{self._mega_id}', pt_idx), (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_idx}' if not isinstance(self.port, list) else f'{self._mega_id} {pt_idx}', 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:
# self._lg = self.mega.lg.getChild(self._name or self.unique_id)
return _LOGGER return _LOGGER
@property @property
@@ -431,6 +421,8 @@ class MegaOutPort(MegaPushEntity):
)) ))
def _calc_brightness(self, brightness): def _calc_brightness(self, brightness):
if brightness is None:
brightness = 0
pct = brightness / 255 pct = brightness / 255
pct = max((0, pct)) pct = max((0, pct))
pct = min((pct, 1)) pct = min((pct, 1))
@@ -440,6 +432,8 @@ class MegaOutPort(MegaPushEntity):
return brightness return brightness
def _cal_reverse_brightness(self, brightness): def _cal_reverse_brightness(self, brightness):
if brightness is None:
brightness = 0
l, h = self.range l, h = self.range
d = h - l d = h - l
pct = (brightness - l) / d pct = (brightness - l) / d

View File

@@ -134,7 +134,8 @@ class MegaView(HomeAssistantView):
if ret == 'd' and act: if ret == 'd' and act:
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 ''
elif port in hub.binary_sensors: 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)

View File

@@ -9,51 +9,50 @@ import re
import json import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from homeassistant.components.sensor import SensorDeviceClass
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import TEMP_CELSIUS, PERCENTAGE, LIGHT_LUX
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_ILLUMINANCE, 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,
REMOVE_CONFIG 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, safe_int 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
} }
@@ -62,45 +61,45 @@ 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,
lg: logging.Logger, lg: logging.Logger,
id: str, id: str,
config: ConfigEntry = None, config: ConfigEntry = None,
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, smooth: list = None,
**kwargs, **kwargs,
): ):
"""Initialize.""" """Initialize."""
self.skip_ports = set() self.skip_ports = set()
if config is not None: if config is not None:
lg.debug(f'load config: %s', config.data) lg.debug(f"load config: %s", config.data)
self.config = config self.config = config
self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP) self.http = hass.data.get(DOMAIN, {}).get(CONF_HTTP)
if not self.http is None: if not self.http is None:
self.http.allowed_hosts |= {host} self.http.allowed_hosts |= {host}
self.http.hubs[host] = self self.http.hubs[host] = self
if len(self.http.hubs) == 1: if len(self.http.hubs) == 1:
self.http.hubs['__def'] = self self.http.hubs["__def"] = self
if mqtt_id: if mqtt_id:
self.http.hubs[mqtt_id] = self self.http.hubs[mqtt_id] = self
self.smooth = smooth or [] self.smooth = smooth or []
@@ -134,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 = {}
@@ -144,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]
@@ -159,11 +162,11 @@ class MegaD:
self.customize[CONF_FORCE_D] = force_d self.customize[CONF_FORCE_D] = force_d
try: try:
if allow_hosts is not None and DOMAIN in hass.data: 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 = [] self.binary_sensors = []
async def start(self): async def start(self):
@@ -183,7 +186,7 @@ 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
@@ -211,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
@@ -236,13 +241,10 @@ 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)
try: try:
await self.get_port( await self.get_port(
port=x, port=x, force_http=True, http_cmd="list", conv=False
force_http=True,
http_cmd='list',
conv=False
) )
except asyncio.TimeoutError: except asyncio.TimeoutError:
continue continue
@@ -251,7 +253,7 @@ class MegaD:
""" """
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:
@@ -264,7 +266,7 @@ class MegaD:
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)
@@ -275,14 +277,14 @@ class MegaD:
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()
@@ -292,84 +294,90 @@ 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):
for _ntry in range(3): for _ntry in range(3):
try: try:
async with aiohttp.request("get", url=url, timeout=aiohttp.ClientTimeout(total=5)) as req: async with aiohttp.request(
"get", url=url, timeout=aiohttp.ClientTimeout(total=5)
) as req:
if req.status != 200: if req.status != 200:
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(encoding="iso-8859-5"),
)
return None return None
else: else:
ret = await req.text() ret = await req.text(encoding="iso-8859-5")
self.lg.debug('response %s', ret) self.lg.debug("response %s", ret)
return ret return ret
except asyncio.TimeoutError: except asyncio.TimeoutError:
self.lg.warning(f'timeout while requesting {url}') self.lg.warning(f"timeout while requesting {url}")
# raise # raise
await asyncio.sleep(1) await asyncio.sleep(1)
raise asyncio.TimeoutError('after 3 tries') 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 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(await self.request(pt=port, cmd=http_cmd), cmd=http_cmd) ret = self.parse_response(
await self.request(pt=port, cmd=http_cmd), cmd=http_cmd
)
ntry = 0 ntry = 0
while http_cmd == 'list' and ret is None and ntry < 3: while http_cmd == "list" and ret is None and ntry < 3:
await asyncio.sleep(1) await asyncio.sleep(1)
ret = self.parse_response(await self.request(pt=port, cmd=http_cmd)) ret = self.parse_response(await self.request(pt=port, cmd=http_cmd))
ntry += 1 ntry += 1
self.lg.debug('parsed: %s', ret) self.lg.debug("parsed: %s", ret)
self.values[port] = ret self.values[port] = ret
return ret return ret
@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):
try: try:
ret = await self.request(cmd='all') ret = await self.request(cmd="all")
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
for port, x in enumerate(ret.split(';')): for port, x in enumerate(ret.split(";")):
if port in self.ds2413_ports: if port in self.ds2413_ports:
continue continue
if check_skip and not port in self.ports: if check_skip and not port in self.ports:
@@ -387,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)
@@ -413,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)
@@ -427,14 +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, str(callback) f"subscribe %s",
port,
) )
self.http.callbacks[self.id][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."""
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:
@@ -442,21 +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 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):
""" """
@@ -465,12 +472,12 @@ class MegaD:
:return: :return:
""" """
try: try:
values = await self.request(pt=port, cmd='get') values = await self.request(pt=port, cmd="get")
except asyncio.TimeoutError: except asyncio.TimeoutError:
return 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):
@@ -479,130 +486,161 @@ class MegaD:
:param params: параметры url :param params: параметры url
:return: :return:
""" """
pt = params.get('pt') pt = params.get("pt")
if pt in self.skip_ports: if pt in self.skip_ports:
return return
if pt is not None: if pt is not None:
pass 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")
try: try:
ret = { ret = {_params: await self.request(**params)}
_params: await self.request(**params)
}
except asyncio.TimeoutError: except asyncio.TimeoutError:
return return
self.lg.debug('i2c response: %s', ret) 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 = [] 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):
if cfg.misc is not None: if cfg.misc is not None:
smooth.append(port) smooth.append(port)
ret['light'][port].append({'dimmer': cfg.m == '1', 'smooth': safe_int(cfg.misc)}) 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}' if not self.new_naming else f'{port:02d}e{n:02d}' 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)):
pt = f'{port}e{n}' pt = f"{port}e{n}"
name = pt if not self.new_naming else f'{port:02}e{n:02}' 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}'}) ret["light"][pt].append(
if cfg.pty == '4': # and (cfg.gr == '0' or _cust.get(CONF_FORCE_I2C_SCAN)) {
"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 is None and nports < 30: elif cfg.pty is None and nports < 30:
# вроде как это ADC на 328 меге # вроде как это ADC на 328 меге
ret['sensor'][port].append(dict()) ret["sensor"][port].append(dict())
elif cfg.pty in ('3', '2', '4'): elif cfg.pty in ("3", "2", "4"):
http_cmd = 'get' http_cmd = "get"
if cfg.d == '5' and cfg.pty == '3': 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):
@@ -614,10 +652,7 @@ 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): async def reload(self, reload_entry=True):
new = await self.get_config(nports=self.nports) new = await self.get_config(nports=self.nports)
@@ -625,14 +660,14 @@ class MegaD:
for x in REMOVE_CONFIG: for x in REMOVE_CONFIG:
cfg.pop(x, None) cfg.pop(x, None)
cfg.update(new) cfg.update(new)
self.lg.debug(f'new config: %s', cfg) self.lg.debug(f"new config: %s", cfg)
self.config.data = cfg self.config.data = cfg
if reload_entry: if reload_entry:
await self.hass.config_entries.async_reload(self.config.entry_id) await self.hass.config_entries.async_reload(self.config.entry_id)
return cfg return cfg
def _wrap_port_smooth(self, from_, to_, time): def _wrap_port_smooth(self, from_, to_, time):
self.lg.debug('dim from %s to %s for %s seconds', from_, to_, time) self.lg.debug("dim from %s to %s for %s seconds", from_, to_, time)
if time <= 0: if time <= 0:
return return
beg = datetime.now() beg = datetime.now()
@@ -645,15 +680,15 @@ class MegaD:
yield val yield val
async def smooth_dim( async def smooth_dim(
self, self,
*config: typing.Tuple[typing.Any, int, int], *config: typing.Tuple[typing.Any, int, int],
time: float, time: float,
jitter: int = 50, jitter: int = 50,
ws=False, ws=False,
updater=None, updater=None,
can_smooth_hardware=False, can_smooth_hardware=False,
max_values=None, max_values=None,
chip=None, chip=None,
): ):
""" """
Плавное диммирование силами сервера, сразу нескольких портов (одной командой) Плавное диммирование силами сервера, сразу нескольких портов (одной командой)
@@ -697,7 +732,9 @@ class MegaD:
continue continue
if not ws: if not ws:
cmd = dict( cmd = dict(
cmd=';'.join([f'{pt}:{_next_val[i]}' for i, (pt, _, _) in enumerate(config)]) cmd=";".join(
[f"{pt}:{_next_val[i]}" for i, (pt, _, _) in enumerate(config)]
)
) )
await self.request(**cmd) await self.request(**cmd)
else: else:
@@ -705,7 +742,9 @@ class MegaD:
cmd = dict( cmd = dict(
pt=config[0][0], pt=config[0][0],
chip=chip, chip=chip,
ws=''.join([hex(x).split('x')[1].rjust(2, '0').upper() for x in _next_val]) ws="".join(
[hex(x).split("x")[1].rjust(2, "0").upper() for x in _next_val]
),
) )
await self.request(**cmd) await self.request(**cmd)

View File

@@ -1,12 +1,10 @@
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,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_CO2,
PERCENTAGE, PERCENTAGE,
LIGHT_LUX, LIGHT_LUX,
TEMP_CELSIUS, TEMP_CELSIUS,
@@ -15,7 +13,15 @@ from homeassistant.const import (
) )
from collections import namedtuple from collections import namedtuple
DeviceType = namedtuple('DeviceType', 'device_class,unit_of_measurement,suffix')
# 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):
@@ -32,39 +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, DeviceType): elif isinstance(c, DeviceType):
c, m, suffix = c c, m, suffix, delay = astuple(c)
if delay is not None:
_params['delay'] = delay
else: else:
continue continue
suffix = suffix or c suffix = suffix or c
if 'addr' in params: if 'addr' in _params:
suffix += f"_{params['addr']}" if suffix else str(params['addr']) 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, 'unit_of_measurement': m,
}) })
req.append(params) req.append(_params)
return req, ret return req, ret
@@ -79,55 +86,55 @@ class Request:
i2c_classes = { i2c_classes = {
'htu21d': [ 'htu21d': [
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'sht31': [ 'sht31': [
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None), DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None, delay=1.5),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'max44009': [ 'max44009': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
], ],
'bh1750': [ 'bh1750': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
], ],
'tsl2591': [ 'tsl2591': [
DeviceType(DEVICE_CLASS_ILLUMINANCE, LIGHT_LUX, None) DeviceType(SensorDeviceClass.ILLUMINANCE, LIGHT_LUX, None)
], ],
'bmp180': [ 'bmp180': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'bmx280': [ 'bmx280': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
DeviceType(DEVICE_CLASS_HUMIDITY, PERCENTAGE, None) DeviceType(SensorDeviceClass.HUMIDITY, PERCENTAGE, None)
], ],
'dps368': [ 'dps368': [
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mlx90614': [ 'mlx90614': [
Skip, Skip,
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'temp'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'temp'),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, 'object'), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, 'object'),
], ],
'ptsensor': [ 'ptsensor': [
Skip, Skip,
Request(delay=1), # запрос на измерение Request(delay=3), # запрос на измерение
DeviceType(DEVICE_CLASS_PRESSURE, PRESSURE_BAR, None), DeviceType(SensorDeviceClass.PRESSURE, PRESSURE_BAR, None),
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'mcp9600': [ 'mcp9600': [
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # термопара DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # термопара
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None), # сенсор встроенный в микросхему
], ],
't67xx': [ 't67xx': [
DeviceType(DEVICE_CLASS_CO2, CONCENTRATION_PARTS_PER_MILLION, None) DeviceType(SensorDeviceClass.CO2, CONCENTRATION_PARTS_PER_MILLION, None)
], ],
'tmp117': [ 'tmp117': [
DeviceType(DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None), DeviceType(SensorDeviceClass.TEMPERATURE, TEMP_CELSIUS, None),
], ],
'ads1115': [ 'ads1115': [
DeviceType(None, None, 'ch0'), DeviceType(None, None, 'ch0'),

View File

@@ -1,4 +1,6 @@
"""Platform for light integration.""" """Platform for light integration."""
from __future__ import annotations
import asyncio import asyncio
import logging import logging
import typing import typing
@@ -15,7 +17,9 @@ from homeassistant.components.light import (
LightEntity, LightEntity,
SUPPORT_TRANSITION, SUPPORT_TRANSITION,
SUPPORT_COLOR, SUPPORT_COLOR,
SUPPORT_WHITE_VALUE 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 (
@@ -34,7 +38,15 @@ from .const import (
CONF_SWITCH, CONF_SWITCH,
DOMAIN, DOMAIN,
CONF_CUSTOM, CONF_CUSTOM,
CONF_SKIP, CONF_LED, CONF_WS28XX, CONF_PORTS, CONF_WHITE_SEP, CONF_SMOOTH, CONF_ORDER, CONF_CHIP, RGB, CONF_SKIP,
CONF_LED,
CONF_WS28XX,
CONF_PORTS,
CONF_WHITE_SEP,
CONF_SMOOTH,
CONF_ORDER,
CONF_CHIP,
RGB,
) )
from .tools import int_ignore, map_reorder_rgb from .tools import int_ignore, map_reorder_rgb
@@ -62,13 +74,17 @@ 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, {}).get(mid, {}) customize = hass.data.get(DOMAIN, {}).get(CONF_CUSTOM, {}).get(mid, {})
skip = [] skip = []
@@ -76,23 +92,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
for entity_id, conf in customize[CONF_LED].items(): for entity_id, conf in customize[CONF_LED].items():
ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)] ports = conf.get(CONF_PORTS) or [conf.get(CONF_PORT)]
skip.extend(ports) skip.extend(ports)
devices.append(MegaRGBW( devices.append(
mega=hub, MegaRGBW(
port=ports, mega=hub,
name=entity_id, port=ports,
customize=conf, name=entity_id,
id_suffix=entity_id, customize=conf,
config_entry=config_entry id_suffix=entity_id,
)) config_entry=config_entry,
for port, cfg in config_entry.data.get('light', {}).items(): )
)
for port, cfg in config_entry.data.get("light", {}).items():
port = int_ignore(port) port = int_ignore(port)
c = customize.get(port, {}) c = customize.get(port, {})
if c.get(CONF_SKIP, False) or port in skip 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)
@@ -100,27 +122,25 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
class MegaLight(MegaOutPort, LightEntity): class MegaLight(MegaOutPort, LightEntity):
@property @property
def supported_features(self): def supported_features(self):
return ( return (SUPPORT_BRIGHTNESS if self.dimmer else 0) | (
(SUPPORT_BRIGHTNESS if self.dimmer else 0) | SUPPORT_TRANSITION if self.dimmer else 0
(SUPPORT_TRANSITION if self.dimmer else 0)
) )
class MegaRGBW(LightEntity, BaseMegaEntity): class MegaRGBW(LightEntity, BaseMegaEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self._is_on = None self._is_on = None
self._brightness = None self._brightness = None
self._hs_color = None self._hs_color = None
self._rgb_color: tuple[int, int, int] | None = None
self._white_value = None self._white_value = None
self._task: asyncio.Task = None self._task: asyncio.Task = None
self._restore = None self._restore = None
self.smooth: timedelta = self.customize[CONF_SMOOTH] self.smooth: timedelta = self.customize[CONF_SMOOTH]
self._color_order = self.customize.get(CONF_ORDER, 'rgb') self._color_order = self.customize.get(CONF_ORDER, "rgb")
self._last_called: float = 0 self._last_called: float = 0
self._max_values = None self._max_values = None
@@ -128,7 +148,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
def max_values(self) -> list: def max_values(self) -> list:
if self._max_values is None: if self._max_values is None:
if self.is_ws: if self.is_ws:
self._max_values = [255] * 3 self._max_values = [255] * 4
else: else:
self._max_values = [ self._max_values = [
255 if isinstance(x, int) else 4095 for x in self.port 255 if isinstance(x, int) else 4095 for x in self.port
@@ -143,37 +163,55 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
def is_ws(self): def is_ws(self):
return self.customize.get(CONF_WS28XX) 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 @property
def white_value(self): def white_value(self):
if self.supported_features & SUPPORT_WHITE_VALUE: # if self.supported_features & SUPPORT_WHITE_VALUE:
return float(self.get_attribute('white_value', 0)) 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 @property
def brightness(self): def brightness(self):
return float(self.get_attribute('brightness', 0)) return float(self.get_attribute("brightness", 0))
@property @property
def hs_color(self): def hs_color(self):
return self.get_attribute('hs_color', [0, 0]) return self.get_attribute("hs_color", [0, 0])
@property @property
def is_on(self): def is_on(self):
return self.get_attribute('is_on', False) return self.get_attribute("is_on", False)
@property @property
def supported_features(self): def supported_features(self):
return ( return LightEntityFeature.TRANSITION
SUPPORT_BRIGHTNESS |
SUPPORT_TRANSITION |
SUPPORT_COLOR |
(SUPPORT_WHITE_VALUE if len(self.port) == 4 else 0)
)
def get_rgbw(self): def get_rgbw(self):
if not self.is_on: if not self.is_on:
return [0 for x in range(len(self.port))] if not self.is_ws else [0] * 3 return [0 for x in range(len(self.port))] if not self.is_ws else [0] * 3
rgb = colorsys.hsv_to_rgb( rgb = colorsys.hsv_to_rgb(
self.hs_color[0]/360, self.hs_color[1]/100, self.brightness / 255 self.hs_color[0] / 360, self.hs_color[1] / 100, self.brightness / 255
) )
rgb = [x for x in rgb] rgb = [x for x in rgb]
if self.white_value is not None: if self.white_value is not None:
@@ -181,9 +219,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
if not self.customize.get(CONF_WHITE_SEP): if not self.customize.get(CONF_WHITE_SEP):
white = white * (self.brightness / 255) white = white * (self.brightness / 255)
rgb.append(white / 255) rgb.append(white / 255)
rgb = [ rgb = [round(x * self.max_values[i]) for i, x in enumerate(rgb)]
round(x * self.max_values[i]) for i, x in enumerate(rgb)
]
if self.is_ws: if self.is_ws:
# восстанавливаем мэпинг # восстанавливаем мэпинг
rgb = map_reorder_rgb(rgb, RGB, self._color_order) rgb = map_reorder_rgb(rgb, RGB, self._color_order)
@@ -193,7 +229,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
if (time.time() - self._last_called) < 0.1: if (time.time() - self._last_called) < 0.1:
return return
self._last_called = time.time() self._last_called = time.time()
self.lg.debug(f'turn on %s with kwargs %s', self.entity_id, kwargs) self.lg.debug(f"turn on %s with kwargs %s", self.entity_id, kwargs)
if self._restore is not None: if self._restore is not None:
self._restore.update(kwargs) self._restore.update(kwargs)
kwargs = self._restore kwargs = self._restore
@@ -209,9 +245,9 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
return return
self._last_called = time.time() self._last_called = time.time()
self._restore = { self._restore = {
'hs_color': self.hs_color, "hs_color": self.hs_color,
'brightness': self.brightness, "brightness": self.brightness,
'white_value': self.white_value, "white_value": self.white_value,
} }
_before = self.get_rgbw() _before = self.get_rgbw()
self._is_on = False self._is_on = False
@@ -220,17 +256,21 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
self._task = asyncio.create_task(self.set_color(_before, **kwargs)) self._task = asyncio.create_task(self.set_color(_before, **kwargs))
async def set_color(self, _before, **kwargs): async def set_color(self, _before, **kwargs):
transition = kwargs.get('transition') transition = kwargs.get("transition")
update_state = transition is not None and transition > 3 update_state = transition is not None and transition > 3
_after = None
for item, value in kwargs.items(): for item, value in kwargs.items():
setattr(self, f'_{item}', value) setattr(self, f"_{item}", value)
_after = self.get_rgbw() if item == "rgb_color":
_after = map_reorder_rgb(value, RGB, self._color_order)
_after = _after or self.get_rgbw()
self._rgb_color = tuple(_after[:3])
if transition is None: if transition is None:
transition = self.smooth.total_seconds() transition = self.smooth.total_seconds()
ratio = self.calc_speed_ratio(_before, _after) ratio = self.calc_speed_ratio(_before, _after)
transition = transition * ratio transition = transition * ratio
self.async_write_ha_state() self.async_write_ha_state()
ports = self.port if not self.is_ws else self.port*3 ports = self.port if not self.is_ws else self.port * 3
config = [(port, _before[i], _after[i]) for i, port in enumerate(ports)] config = [(port, _before[i], _after[i]) for i, port in enumerate(ports)]
try: try:
await self.mega.smooth_dim( await self.mega.smooth_dim(
@@ -246,7 +286,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
except asyncio.CancelledError: except asyncio.CancelledError:
return return
except: except:
self.lg.exception('while dimming') self.lg.exception("while dimming")
async def async_will_remove_from_hass(self) -> None: async def async_will_remove_from_hass(self) -> None:
await super().async_will_remove_from_hass() await super().async_will_remove_from_hass()
@@ -261,10 +301,10 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
w = None w = None
rgb = rgbw rgb = rgbw
if self.is_ws: if self.is_ws:
rgb = map_reorder_rgb( rgb = map_reorder_rgb(rgb, self._color_order, 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, s, v = colorsys.rgb_to_hsv(*[x/self.max_values[i] for i, x in enumerate(rgb)]) )
h *= 360 h *= 360
s *= 100 s *= 100
v *= 255 v *= 255
@@ -273,7 +313,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
self._brightness = v self._brightness = v
if w is not None: if w is not None:
if not self.customize.get(CONF_WHITE_SEP): if not self.customize.get(CONF_WHITE_SEP):
w = w/(self._brightness / 255) w = w / (self._brightness / 255)
else: else:
w = w w = w
w = w / (self.max_values[-1] / 255) w = w / (self.max_values[-1] / 255)
@@ -298,7 +338,7 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
return return
data = data.get(x, None) data = data.get(x, None)
if isinstance(data, dict): if isinstance(data, dict):
data = data.get('value') data = data.get("value")
data = safe_int(data) data = safe_int(data)
if data is None: if data is None:
return return
@@ -315,4 +355,4 @@ class MegaRGBW(LightEntity, BaseMegaEntity):
ret = r ret = r
else: else:
ret = max([r, ret]) ret = max([r, ret])
return ret return ret

View File

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

View File

@@ -4,9 +4,7 @@ import voluptuous as vol
import struct 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 (
@@ -40,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 = {
@@ -108,30 +106,34 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, asyn
async_add_devices(devices) async_add_devices(devices)
class FilterBadValues(MegaPushEntity): class FilterBadValues(MegaPushEntity, SensorEntity):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self._prev_value = None self._prev_value = None
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def filter_value(self, value): def filter_value(self, value):
if value \ try:
in self.filter_values \ if value \
or (self.filter_low is not None and value < self.filter_low) \ in self.filter_values \
or (self.filter_high is not None and value > self.filter_high) \ or (self.filter_low is not None and value < self.filter_low) \
or ( or (self.filter_high is not None and value > self.filter_high) \
self._prev_value is not None or (
and self.filter_scale is not None self._prev_value is not None
and ( and self.filter_scale is not None
abs(value - self._prev_value) / self._prev_value > self.filter_scale and (
) abs(value - self._prev_value) / self._prev_value > self.filter_scale
)
): ):
if self.fill_na == 'last': if self.fill_na == 'last':
value = self._prev_value value = self._prev_value
else: else:
value = None value = None
self._prev_value = value self._prev_value = value
return value return value
except Exception as exc:
lg.exception(f'while parsing value')
return None
@property @property
def filter_values(self): def filter_values(self):
@@ -191,28 +193,32 @@ class MegaI2C(FilterBadValues):
return self._device_class return self._device_class
@property @property
def unit_of_measurement(self): def native_unit_of_measurement(self):
return self._unit_of_measurement return self._unit_of_measurement
@property @property
def state(self): def native_value(self):
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: try:
ret = float(ret) ret = self.mega.values.get(self._params)
if tmpl is not None and self.hass is not None: if self.customize.get(CONF_HEX_TO_FLOAT):
tmpl.hass = self.hass try:
ret = tmpl.async_render({'value': ret}) ret = struct.unpack('!f', bytes.fromhex(ret))[0]
except: except:
ret = ret self.lg.warning(f'could not convert {ret} form hex to float')
ret = self.filter_value(ret) tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
if ret is not None: try:
return str(ret) 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 @property
def device_class(self): def device_class(self):
@@ -223,8 +229,8 @@ class Mega1WSensor(FilterBadValues):
def __init__( def __init__(
self, self,
unit_of_measurement, unit_of_measurement=None,
device_class, device_class=None,
key=None, key=None,
*args, *args,
**kwargs **kwargs
@@ -244,7 +250,7 @@ class Mega1WSensor(FilterBadValues):
self.prev_value = None 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
@@ -275,56 +281,63 @@ class Mega1WSensor(FilterBadValues):
return self._device_class return self._device_class
@property @property
def state(self): def native_value(self):
ret = None try:
if not hasattr(self, 'key'): ret = None
return None if not hasattr(self, 'key'):
if self.key: return None
try: if self.key:
ret = self.mega.values.get(self.port, {}) try:
if isinstance(ret, dict): ret = self.mega.values.get(self.port, {})
ret = ret.get('value', {})
if isinstance(ret, dict): if isinstance(ret, dict):
ret = ret.get(self.key) ret = ret.get('value', {})
except: if isinstance(ret, dict):
self.lg.error(self.mega.values.get(self.port, {}).get('value', {})) ret = ret.get(self.key)
return except:
else: self.lg.error(self.mega.values.get(self.port, {}).get('value', {}))
ret = self.mega.values.get(self.port, {}).get('value') return
if ret is None and self.fill_na == 'fill_na' and self.prev_value is not None: else:
ret = self.prev_value ret = self.mega.values.get(self.port, {}).get('value')
elif ret is None and self.fill_na == 'fill_na' and self._state is not None: if ret is None and self.fill_na == 'fill_na' and self.prev_value is not None:
ret = self._state.state ret = self.prev_value
try: elif ret is None and self.fill_na == 'fill_na' and self._state is not None:
ret = float(ret) ret = self._state.state
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: try:
ret = struct.unpack('!f', bytes.fromhex(ret))[0] ret = float(ret)
ret = str(ret)
except: except:
self.lg.warning(f'could not convert {ret} form hex to float') self.lg.debug(f'could not convert to float "{ret}"')
tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE)) ret = self.prev_value
try: if self.customize.get(CONF_HEX_TO_FLOAT):
ret = float(ret) try:
if tmpl is not None and self.hass is not None: ret = struct.unpack('!f', bytes.fromhex(ret))[0]
tmpl.hass = self.hass except:
ret = tmpl.async_render({'value': ret}) self.lg.warning(f'could not convert {ret} form hex to float')
except: tmpl: Template = self.customize.get(CONF_CONV_TEMPLATE, self.customize.get(CONF_VALUE_TEMPLATE))
pass try:
ret = self.filter_value(ret) ret = float(ret)
self.prev_value = ret if tmpl is not None and self.hass is not None:
if ret is not None: tmpl.hass = self.hass
return str(ret) 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

View File

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

View File

@@ -12,7 +12,7 @@
Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если Если вам понравилась интеграция, не забудьте поставить звезду на гитхабе - вам не сложно, а мне приятно ) А если
интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната ) интеграция очень понравилась - еще приятнее, если вы воспользуетесь кнопкой доната )
Обновление прошивки MegaD можно делать прямо из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git) Обновление прошивки MegaD можно делать из HA с помощью [аддона](https://github.com/andvikt/mega_addon.git)
## Основные особенности {: #mains } ## Основные особенности {: #mains }
- Настройка в [веб-интерфейсе](settings.md) + [yaml](yaml.md) - Настройка в [веб-интерфейсе](settings.md) + [yaml](yaml.md)

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
# MegaD HomeAssistant integration # MegaD HomeAssistant integration
[![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/custom-components/hacs) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-orange.svg)](https://github.com/hacs/integration)
[![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/410013955329136) [![Donate](https://img.shields.io/badge/donate-Yandex-red.svg)](https://yoomoney.ru/to/410013955329136)
Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561) Интеграция с [MegaD-2561, MegaD-328](https://www.ab-log.ru/smart-house/ethernet/megad-2561)