mirror of
https://github.com/andvikt/mega_hacs.git
synced 2025-12-08 07:34:27 +05:00
initial
This commit is contained in:
36
custom_components/mega/.experiment.py
Normal file
36
custom_components/mega/.experiment.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
from bs4 import BeautifulSoup
|
||||
import aiohttp
|
||||
|
||||
host = '192.168.88.14/sec'
|
||||
|
||||
|
||||
|
||||
|
||||
# page = '''
|
||||
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&cmd=7:1">ON</a> <a href="/sec/?pt=7&cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
|
||||
# <head><style>input,select{margin:1px}</style></head>
|
||||
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&cmd=7:1">ON</a> <a href="/sec/?pt=7&cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
|
||||
# <a href="/sec/?cf=3">Back</a>
|
||||
# <br>
|
||||
# P7/ON
|
||||
# <br>
|
||||
# <a href="/sec/?pt=7&cmd=7:1">ON</a>
|
||||
# <a href="/sec/?pt=7&cmd=7:0">OFF</a>
|
||||
# <br>
|
||||
# <form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form>
|
||||
# <body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&cmd=7:1">ON</a> <a href="/sec/?pt=7&cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body>
|
||||
# <html><head><style>input,select{margin:1px}</style></head><body><a href="/sec/?cf=3">Back</a><br>P7/ON<br><a href="/sec/?pt=7&cmd=7:1">ON</a> <a href="/sec/?pt=7&cmd=7:0">OFF</a><br><form action="/sec/"><input type="hidden" name="pn" value="7">Type <select name="pty"><option value="255">NC</option><option value="0">In</option><option value="1" selected="">Out</option><option value="3">DSen</option><option value="4">I2C</option></select><br>Default: <select name="d"><option value="0" selected="">0</option><option value="1">1</option></select><br>Mode <select name="m"><option value="0" selected="">SW</option><option value="3">SW LINK</option><option value="2">DS2413</option></select><br>Group <input name="grp" size="2" value=""><br><input type="submit" value="Save"></form></body></html>
|
||||
# '''
|
||||
# tree = BeautifulSoup(page, features="lxml")
|
||||
# pty = tree.find('select', attrs={'name': 'pty'}).find(selected=True)['value']
|
||||
# m = tree.find('select', attrs={'name': 'm'})
|
||||
# if m:
|
||||
# m = m.find(selected=True)['value']
|
||||
#
|
||||
# print(pty, m)
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.get_event_loop().run_until_complete(
|
||||
scan_port(0)
|
||||
)
|
||||
53
custom_components/mega/.idea/workspace.xml
generated
Normal file
53
custom_components/mega/.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="4e5226d4-ea76-41c3-866f-a293e2a36ee1" name="Default Changelist" comment="">
|
||||
<change beforePath="$PROJECT_DIR$/readme.md" beforeDir="false" afterPath="$PROJECT_DIR$/readme.md" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="ProjectId" id="1m1ke0h9bWkC4NQvoBSN4CpZPQ8" />
|
||||
<component name="ProjectLevelVcsManager" settingsEditedManually="true" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">
|
||||
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
||||
<property name="WebServerToolWindowFactoryState" value="true" />
|
||||
<property name="last_opened_file_path" value="$PROJECT_DIR$" />
|
||||
<property name="node.js.detected.package.eslint" value="true" />
|
||||
<property name="node.js.path.for.package.eslint" value="project" />
|
||||
<property name="node.js.selected.package.eslint" value="(autodetect)" />
|
||||
<property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
|
||||
<property name="nodejs_npm_path_reset_for_default_project" value="true" />
|
||||
<property name="settings.editor.selected.configurable" value="com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable" />
|
||||
</component>
|
||||
<component name="RecentsManager">
|
||||
<key name="CopyFile.RECENT_KEYS">
|
||||
<recent name="$PROJECT_DIR$" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="4e5226d4-ea76-41c3-866f-a293e2a36ee1" name="Default Changelist" comment="" />
|
||||
<created>1608668575985</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1608668575985</updated>
|
||||
<workItem from="1608668577233" duration="464000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
</project>
|
||||
126
custom_components/mega/__init__.py
Normal file
126
custom_components/mega/__init__.py
Normal file
@@ -0,0 +1,126 @@
|
||||
"""The mega integration."""
|
||||
import asyncio
|
||||
import logging
|
||||
import typing
|
||||
from functools import partial
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_SCAN_INTERVAL, CONF_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.service import bind_hass
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from .const import DOMAIN, CONF_INVERT
|
||||
from .hub import MegaD
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
CONF_MQTT_ID = "mqtt_id"
|
||||
CONF_PORT_TO_SCAN = 'port_to_scan'
|
||||
|
||||
MEGA = {
|
||||
vol.Required(CONF_HOST): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
vol.Optional(CONF_MQTT_ID, default=""): str,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=60): int,
|
||||
vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
|
||||
}
|
||||
MEGA_MAPPED = {str: MEGA}
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
{
|
||||
DOMAIN: vol.Any(MEGA, MEGA_MAPPED)
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
PLATFORMS = [
|
||||
"light",
|
||||
"binary_sensor",
|
||||
"sensor",
|
||||
]
|
||||
ALIVE_STATE = 'alive'
|
||||
DEF_ID = 'def'
|
||||
_POLL_TASKS = {}
|
||||
_hubs = {}
|
||||
_subs = {}
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the mega component."""
|
||||
conf = config.get(DOMAIN)
|
||||
hass.data[DOMAIN] = {}
|
||||
if conf is None:
|
||||
return True
|
||||
if CONF_HOST in conf:
|
||||
conf = {DEF_ID: conf}
|
||||
for id, data in conf.items():
|
||||
await _add_mega(hass, id, data)
|
||||
hass.services.async_register(
|
||||
DOMAIN, 'save', _save_service,
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def _add_mega(hass: HomeAssistant, id, data: dict):
|
||||
data.update(id=id)
|
||||
_mqtt = hass.data.get(mqtt.DOMAIN)
|
||||
if _mqtt is None:
|
||||
raise Exception('mqtt not configured, please configure mqtt first')
|
||||
hass.data[DOMAIN][id] = hub = MegaD(hass, **data, mqtt=_mqtt, lg=_LOGGER)
|
||||
if not await hub.authenticate():
|
||||
raise Exception("not authentificated")
|
||||
mid = await hub.get_mqtt_id()
|
||||
hub.mqtt_id = mid
|
||||
_POLL_TASKS[id] = asyncio.create_task(hub.poll())
|
||||
return hub
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
print(entry.entry_id)
|
||||
id = entry.data.get('id', entry.entry_id)
|
||||
hub = await _add_mega(hass, id, dict(entry.data))
|
||||
_hubs[entry.entry_id] = hub
|
||||
_subs[entry.entry_id] = entry.add_update_listener(update)
|
||||
for platform in PLATFORMS:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(
|
||||
entry, platform
|
||||
)
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
async def update(hass: HomeAssistant, entry: ConfigEntry):
|
||||
hub: MegaD = hass.data[DOMAIN][entry.data[CONF_ID]]
|
||||
hub.poll_interval = entry.options[CONF_SCAN_INTERVAL]
|
||||
hub.port_to_scan = entry.options[CONF_PORT_TO_SCAN]
|
||||
# hub.inverted = map(lambda x: x.strip(), (
|
||||
# entry.options.get(CONF_INVERT, '').split(',')
|
||||
# )
|
||||
return True
|
||||
|
||||
|
||||
async def async_remove_entry(hass, entry) -> None:
|
||||
"""Handle removal of an entry."""
|
||||
id = entry.data.get('id', entry.entry_id)
|
||||
hass.data[DOMAIN][id].unsubscribe_all()
|
||||
task: asyncio.Task = _POLL_TASKS.pop(id)
|
||||
task.cancel()
|
||||
_hubs.pop(entry.entry_id)
|
||||
unsub = _subs.pop(entry.entry_id)
|
||||
unsub()
|
||||
return True
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def _save_service(hass: HomeAssistant, mega_id='def'):
|
||||
hub: MegaD = hass.data[DOMAIN][mega_id]
|
||||
await hub.save()
|
||||
|
||||
|
||||
async def _is_alive(cond: asyncio.Condition, msg):
|
||||
async with cond:
|
||||
cond.notify_all()
|
||||
|
||||
88
custom_components/mega/binary_sensor.py
Normal file
88
custom_components/mega/binary_sensor.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Platform for light integration."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_SCHEMA,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_ID
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .entities import BaseMegaEntity
|
||||
|
||||
from .hub import MegaD
|
||||
|
||||
lg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Validation of the user's configuration
|
||||
_EXTENDED = {
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
vol.Optional(CONF_UNIQUE_ID): str,
|
||||
}
|
||||
_ITEM = vol.Any(int, _EXTENDED)
|
||||
PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(str, description="mega id"): [_ITEM]
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
config.pop(CONF_PLATFORM)
|
||||
ents = []
|
||||
for mid, _config in config.items():
|
||||
for x in _config:
|
||||
if isinstance(x, int):
|
||||
ent = MegaBinarySensor(
|
||||
mega_id=mid, port=x
|
||||
)
|
||||
else:
|
||||
ent = MegaBinarySensor(
|
||||
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME]
|
||||
)
|
||||
ents.append(ent)
|
||||
add_entities(ents)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
|
||||
mid = config_entry.data[CONF_ID]
|
||||
hub: MegaD = hass.data['mega'][mid]
|
||||
devices = []
|
||||
async for port, pty, m in hub.scan_ports():
|
||||
if pty == "0":
|
||||
sensor = MegaBinarySensor(mega_id=mid, port=port)
|
||||
devices.append(sensor)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class MegaBinarySensor(BinarySensorEntity, BaseMegaEntity):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_on = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
if self._is_on is not None:
|
||||
return self._is_on
|
||||
return self._state == 'ON'
|
||||
|
||||
def _update(self, payload: dict):
|
||||
val = payload.get("value")
|
||||
self._is_on = val == 'ON'
|
||||
111
custom_components/mega/config_flow.py
Normal file
111
custom_components/mega/config_flow.py
Normal file
@@ -0,0 +1,111 @@
|
||||
"""Пока не сделано"""
|
||||
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, core
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PASSWORD, CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import callback
|
||||
from .const import DOMAIN, CONF_PORT_TO_SCAN, CONF_RELOAD, CONF_INVERT # pylint:disable=unused-import
|
||||
from .hub import MegaD
|
||||
from . import exceptions
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ID, default='def'): str,
|
||||
vol.Required(CONF_HOST, default="192.168.0.14"): str,
|
||||
vol.Required(CONF_PASSWORD, default="sec"): str,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=60): int,
|
||||
vol.Optional(CONF_PORT_TO_SCAN, default=0): int,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(hass: core.HomeAssistant, data):
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
if data[CONF_ID] in hass.data.get(DOMAIN, []):
|
||||
raise exceptions.DuplicateId('duplicate_id')
|
||||
_mqtt = hass.data.get(mqtt.DOMAIN)
|
||||
if not isinstance(_mqtt, mqtt.MQTT):
|
||||
raise exceptions.MqttNotConfigured("mqtt must be configured first")
|
||||
hub = MegaD(hass, **data, lg=_LOGGER, mqtt=_mqtt)
|
||||
if not await hub.authenticate():
|
||||
raise exceptions.InvalidAuth
|
||||
|
||||
return hub
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for mega."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the initial step."""
|
||||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
|
||||
)
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except exceptions.CannotConnect:
|
||||
errors["base"] = "cannot_connect"
|
||||
except exceptions.InvalidAuth:
|
||||
errors["base"] = "invalid_auth"
|
||||
except exceptions.DuplicateId:
|
||||
errors["base"] = "duplicate_id"
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors[CONF_ID] = str(exc)
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input.get(CONF_ID, user_input[CONF_HOST]),
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
def __init__(self, config_entry: ConfigEntry):
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title='',
|
||||
data={**user_input, **{CONF_ID: self.config_entry.data[CONF_ID]}},
|
||||
)
|
||||
e = self.config_entry.data
|
||||
ret = self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=e[CONF_SCAN_INTERVAL]): int,
|
||||
vol.Optional(CONF_PORT_TO_SCAN, default=e.get(CONF_PORT_TO_SCAN, 0)): int,
|
||||
vol.Optional(CONF_RELOAD, default=False): bool,
|
||||
vol.Optional(CONF_INVERT): vol.Set(),
|
||||
}),
|
||||
)
|
||||
print(ret)
|
||||
return ret
|
||||
14
custom_components/mega/const.py
Normal file
14
custom_components/mega/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants for the mega integration."""
|
||||
|
||||
DOMAIN = "mega"
|
||||
CONF_MEGA_ID = "mega_id"
|
||||
CONF_DIMMER = "dimmer"
|
||||
CONF_SWITCH = "switch"
|
||||
CONF_KEY = 'key'
|
||||
TEMP = 'temp'
|
||||
HUM = 'hum'
|
||||
W1 = 'w1'
|
||||
W1BUS = 'w1bus'
|
||||
CONF_PORT_TO_SCAN = 'port_to_scan'
|
||||
CONF_RELOAD = 'reload'
|
||||
CONF_INVERT = 'invert'
|
||||
74
custom_components/mega/entities.py
Normal file
74
custom_components/mega/entities.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import asyncio
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
from homeassistant.core import State
|
||||
from .hub import MegaD
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class BaseMegaEntity(RestoreEntity):
|
||||
"""
|
||||
Base Mega's entity. It is responsible for storing reference to mega hub
|
||||
Also provides some basic entity information: unique_id, name, availiability
|
||||
It also makes subscription to port states
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
mega_id: str,
|
||||
port: int,
|
||||
id_suffix=None,
|
||||
name=None,
|
||||
unique_id=None
|
||||
):
|
||||
self._state: State = None
|
||||
self.port = port
|
||||
self._name = name
|
||||
self._mega_id = mega_id
|
||||
self._lg = None
|
||||
self._unique_id = unique_id or f"mega_{mega_id}_{port}" + \
|
||||
(f"_{id_suffix}" if id_suffix else "")
|
||||
|
||||
@property
|
||||
def lg(self) -> logging.Logger:
|
||||
if self._lg is None:
|
||||
self._lg = self.mega.lg.getChild(self._name or self.unique_id)
|
||||
return self._lg
|
||||
|
||||
@property
|
||||
def mega(self) -> MegaD:
|
||||
return self.hass.data[DOMAIN][self._mega_id]
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return self.mega.online
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name or f"{self.mega.id}_p{self.port}"
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
return self._unique_id
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
await self.mega.subscribe(self.port, callback=self.__update)
|
||||
self._state = await self.async_get_last_state()
|
||||
await asyncio.sleep(0.1)
|
||||
await self.mega.get_port(self.port)
|
||||
|
||||
def __update(self, msg):
|
||||
try:
|
||||
value = json.loads(msg.payload)
|
||||
except Exception as exc:
|
||||
self.lg.warning(f'could not parse json ({msg.payload}): {exc}')
|
||||
return
|
||||
self._update(value)
|
||||
self.hass.async_create_task(self.async_update_ha_state())
|
||||
self.lg.debug(f'state after update %s', self.state)
|
||||
return
|
||||
|
||||
def _update(self, payload: dict):
|
||||
raise NotImplementedError
|
||||
17
custom_components/mega/exceptions.py
Normal file
17
custom_components/mega/exceptions.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from homeassistant import exceptions
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
||||
|
||||
|
||||
class MqttNotConfigured(exceptions.HomeAssistantError):
|
||||
"""Error to indicate mqtt is not configured"""
|
||||
|
||||
|
||||
class DuplicateId(exceptions.HomeAssistantError):
|
||||
"""Error to indicate duplicate id"""
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indicate there is invalid auth."""
|
||||
259
custom_components/mega/hub.py
Normal file
259
custom_components/mega/hub.py
Normal file
@@ -0,0 +1,259 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from functools import wraps
|
||||
|
||||
import aiohttp
|
||||
import typing
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from homeassistant.components import mqtt
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from .exceptions import CannotConnect
|
||||
|
||||
|
||||
class MegaD:
|
||||
"""MegaD Hub"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
host: str,
|
||||
password: str,
|
||||
mqtt: mqtt.MQTT,
|
||||
lg:logging.Logger,
|
||||
id: str,
|
||||
mqtt_id: str = None,
|
||||
scan_interval=60,
|
||||
port_to_scan=0,
|
||||
inverted:typing.List[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize."""
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.sec = password
|
||||
self.mqtt = mqtt
|
||||
self.id = id
|
||||
self.lck = asyncio.Lock()
|
||||
self.is_alive = asyncio.Condition()
|
||||
self.online = True
|
||||
self.entities: typing.List[Entity] = []
|
||||
self.poll_interval = scan_interval
|
||||
self.subscriptions = []
|
||||
self.lg: logging.Logger = lg.getChild(self.id)
|
||||
self._scanned = {}
|
||||
self.sensors = []
|
||||
self.port_to_scan = port_to_scan
|
||||
self.inverted = inverted or []
|
||||
if not mqtt_id:
|
||||
_id = host.split(".")[-1]
|
||||
self.mqtt_id = f"megad/{_id}"
|
||||
else:
|
||||
self.mqtt_id = mqtt_id
|
||||
self._loop: asyncio.AbstractEventLoop = None
|
||||
|
||||
async def add_entity(self, ent):
|
||||
async with self.lck:
|
||||
self.entities.append(ent)
|
||||
|
||||
async def get_sensors(self):
|
||||
_ports = {x.port for x in self.sensors}
|
||||
for x in _ports:
|
||||
await self.get_port(x)
|
||||
await asyncio.sleep(self.poll_interval)
|
||||
|
||||
async def poll(self):
|
||||
"""
|
||||
Send get port 0 every poll_interval. When answer is received, mega.<id> becomes online else mega.<id> becomes
|
||||
offline
|
||||
"""
|
||||
self._loop = asyncio.get_event_loop()
|
||||
if self.sensors:
|
||||
await self.subscribe(self.sensors[0].port, callback=self._notify)
|
||||
else:
|
||||
await self.subscribe(self.port_to_scan, callback=self._notify)
|
||||
while True:
|
||||
async with self.is_alive:
|
||||
if len(self.sensors) > 0:
|
||||
await self.get_sensors()
|
||||
else:
|
||||
await self.get_port(self.port_to_scan)
|
||||
|
||||
try:
|
||||
await asyncio.wait_for(self.is_alive.wait(), timeout=5)
|
||||
self.hass.states.async_set(
|
||||
f'mega.{self.id}',
|
||||
'online',
|
||||
)
|
||||
self.online = True
|
||||
except asyncio.TimeoutError:
|
||||
self.online = False
|
||||
self.hass.states.async_set(
|
||||
f'mega.{self.id}',
|
||||
'offline',
|
||||
)
|
||||
for x in self.entities:
|
||||
try:
|
||||
await x.async_update_ha_state()
|
||||
except RuntimeError:
|
||||
pass
|
||||
await asyncio.sleep(self.poll_interval)
|
||||
|
||||
async def _async_notify(self):
|
||||
async with self.is_alive:
|
||||
self.is_alive.notify_all()
|
||||
|
||||
def _notify(self, *args):
|
||||
asyncio.run_coroutine_threadsafe(self._async_notify(), self._loop)
|
||||
|
||||
async def get_mqtt_id(self):
|
||||
async with aiohttp.request(
|
||||
'get', f'http://{self.host}/{self.sec}/?cf=2'
|
||||
) as req:
|
||||
data = await req.text()
|
||||
data = BeautifulSoup(data, features="lxml")
|
||||
_id = data.find(attrs={'name': 'mdid'})
|
||||
if _id:
|
||||
_id = _id['value']
|
||||
return _id or 'megad/' + self.host.split('.')[-1]
|
||||
|
||||
async def send_command(self, port=None, cmd=None):
|
||||
if port:
|
||||
url = f"http://{self.host}/{self.sec}/?pt={port}&cmd={cmd}"
|
||||
else:
|
||||
url = f"http://{self.host}/{self.sec}/?cmd={cmd}"
|
||||
self.lg.debug('run command: %s', url)
|
||||
async with self.lck:
|
||||
async with aiohttp.request("get", url=url) as req:
|
||||
if req.status != 200:
|
||||
self.lg.warning('%s returned %s (%s)', url, req.status, await req.text())
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
async def save(self):
|
||||
await self.send_command(cmd='s')
|
||||
|
||||
async def get_port(self, port, get_value=False):
|
||||
if get_value:
|
||||
ftr = asyncio.get_event_loop().create_future()
|
||||
|
||||
def cb(msg):
|
||||
try:
|
||||
ftr.set_result(json.loads(msg.payload).get('value'))
|
||||
except Exception as exc:
|
||||
self.lg.warning(f'could not parse {msg.payload}: {exc}')
|
||||
unsub = await self.mqtt.async_subscribe(
|
||||
topic=f'{self.mqtt_id}/{port}',
|
||||
msg_callback=cb,
|
||||
qos=1,
|
||||
)
|
||||
|
||||
self.lg.debug(
|
||||
f'get port: %s', port
|
||||
)
|
||||
async with self.lck:
|
||||
await self.mqtt.async_publish(
|
||||
topic=f'{self.mqtt_id}/cmd',
|
||||
payload=f'get:{port}',
|
||||
qos=0,
|
||||
retain=False,
|
||||
)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
if get_value:
|
||||
try:
|
||||
return await asyncio.wait_for(ftr, timeout=2)
|
||||
except asyncio.TimeoutError:
|
||||
self.lg.warning(f'timeout on port {port}')
|
||||
finally:
|
||||
unsub()
|
||||
|
||||
async def get_all_ports(self):
|
||||
for x in range(37):
|
||||
await self.get_port(x)
|
||||
|
||||
async def reboot(self, save=True):
|
||||
await self.save()
|
||||
# await self.send_command(cmd=)
|
||||
|
||||
async def subscribe(self, port, callback):
|
||||
|
||||
@wraps(callback)
|
||||
def wrapper(msg):
|
||||
self.lg.debug(
|
||||
'process incomming message: %s', msg
|
||||
)
|
||||
return callback(msg)
|
||||
|
||||
self.lg.debug(
|
||||
f'subscribe %s %s', port, wrapper
|
||||
)
|
||||
subs = await self.mqtt.async_subscribe(
|
||||
topic=f"{self.mqtt_id}/{port}",
|
||||
msg_callback=wrapper,
|
||||
qos=0,
|
||||
)
|
||||
self.subscriptions.append(subs)
|
||||
|
||||
def unsubscribe_all(self):
|
||||
self.lg.info('unsubscribe')
|
||||
for x in self.subscriptions:
|
||||
self.lg.debug('unsubscribe %s', x)
|
||||
x()
|
||||
|
||||
async def authenticate(self) -> bool:
|
||||
"""Test if we can authenticate with the host."""
|
||||
async with aiohttp.request("get", url=f"http://{self.host}/{self.sec}") as req:
|
||||
if "Unauthorized" in await req.text():
|
||||
return False
|
||||
else:
|
||||
if req.status != 200:
|
||||
raise CannotConnect
|
||||
return True
|
||||
|
||||
async def get_port_page(self, port):
|
||||
url = f'http://{self.host}/{self.sec}/?pt={port}'
|
||||
self.lg.debug(f'get page for port {port} {url}')
|
||||
async with aiohttp.request('get', url) as req:
|
||||
return await req.text()
|
||||
|
||||
async def scan_port(self, port):
|
||||
if port in self._scanned:
|
||||
return self._scanned[port]
|
||||
url = f'http://{self.host}/{self.sec}/?pt={port}'
|
||||
self.lg.debug(
|
||||
f'scan port %s: %s', port, url
|
||||
)
|
||||
async with aiohttp.request('get', url) as req:
|
||||
html = await req.text()
|
||||
tree = BeautifulSoup(html, features="lxml")
|
||||
pty = tree.find('select', attrs={'name': 'pty'})
|
||||
if pty is None:
|
||||
return
|
||||
else:
|
||||
pty = pty.find(selected=True)
|
||||
if pty:
|
||||
pty = pty['value']
|
||||
else:
|
||||
return
|
||||
if pty in ['0', '1']:
|
||||
m = tree.find('select', attrs={'name': 'm'})
|
||||
if m:
|
||||
m = m.find(selected=True)['value']
|
||||
self._scanned[port] = (pty, m)
|
||||
return pty, m
|
||||
elif pty == '3':
|
||||
m = tree.find('select', attrs={'name': 'd'})
|
||||
if m:
|
||||
m = m.find(selected=True)['value']
|
||||
self._scanned[port] = (pty, m)
|
||||
return pty, m
|
||||
|
||||
async def scan_ports(self,):
|
||||
for x in range(37):
|
||||
ret = await self.scan_port(x)
|
||||
if ret:
|
||||
yield [x, *ret]
|
||||
154
custom_components/mega/light.py
Normal file
154
custom_components/mega/light.py
Normal file
@@ -0,0 +1,154 @@
|
||||
"""Platform for light integration."""
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
PLATFORM_SCHEMA as LIGHT_SCHEMA,
|
||||
SUPPORT_BRIGHTNESS,
|
||||
LightEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_ID
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .entities import BaseMegaEntity
|
||||
|
||||
from .hub import MegaD
|
||||
from .const import CONF_DIMMER, CONF_SWITCH
|
||||
|
||||
|
||||
lg = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Validation of the user's configuration
|
||||
_EXTENDED = {
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
vol.Optional(CONF_UNIQUE_ID): str,
|
||||
}
|
||||
_ITEM = vol.Any(int, _EXTENDED)
|
||||
DIMMER = {vol.Required(CONF_DIMMER): [_ITEM]}
|
||||
SWITCH = {vol.Required(CONF_SWITCH): [_ITEM]}
|
||||
PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(str, description="mega id"): {
|
||||
vol.Optional("dimmer", default=[]): [_ITEM],
|
||||
vol.Optional("switch", default=[]): [_ITEM],
|
||||
}
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
config.pop(CONF_PLATFORM)
|
||||
ents = []
|
||||
for mid, _config in config.items():
|
||||
for x in _config["dimmer"]:
|
||||
if isinstance(x, int):
|
||||
ent = MegaLight(
|
||||
mega_id=mid, port=x, dimmer=True)
|
||||
else:
|
||||
ent = MegaLight(
|
||||
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=True
|
||||
)
|
||||
ents.append(ent)
|
||||
for x in _config["switch"]:
|
||||
if isinstance(x, int):
|
||||
ent = MegaLight(
|
||||
mega_id=mid, port=x, dimmer=False
|
||||
)
|
||||
else:
|
||||
ent = MegaLight(
|
||||
mega_id=mid, port=x[CONF_PORT], name=x[CONF_NAME], dimmer=False
|
||||
)
|
||||
ents.append(ent)
|
||||
add_entities(ents)
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
|
||||
mid = config_entry.data[CONF_ID]
|
||||
hub: MegaD = hass.data['mega'][mid]
|
||||
devices = []
|
||||
async for port, pty, m in hub.scan_ports():
|
||||
if pty == "1" and m in ['0', '1']:
|
||||
light = MegaLight(mega_id=mid, port=port, dimmer=m == '1')
|
||||
devices.append(light)
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class MegaLight(LightEntity, BaseMegaEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimmer=False,
|
||||
*args, **kwargs
|
||||
):
|
||||
super().__init__(
|
||||
*args, **kwargs
|
||||
)
|
||||
self._brightness = None
|
||||
self._is_on = None
|
||||
self.dimmer = dimmer
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
if self._brightness is not None:
|
||||
return self._brightness
|
||||
if self._state:
|
||||
return self._state.attributes.get("brightness")
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
return SUPPORT_BRIGHTNESS if self.dimmer else 0
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
if self._is_on is not None:
|
||||
return self._is_on
|
||||
return self._state == 'ON'
|
||||
|
||||
async def async_turn_on(self, brightness=None, **kwargs) -> None:
|
||||
brightness = brightness or self.brightness
|
||||
if self.dimmer and brightness == 0:
|
||||
cmd = 255
|
||||
elif self.dimmer:
|
||||
cmd = brightness
|
||||
else:
|
||||
cmd = 1
|
||||
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
|
||||
self._is_on = True
|
||||
self._brightness = brightness
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
|
||||
cmd = "0"
|
||||
|
||||
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
|
||||
self._is_on = False
|
||||
await self.async_update_ha_state()
|
||||
|
||||
def _update(self, payload: dict):
|
||||
val = payload.get("value")
|
||||
try:
|
||||
val = int(val)
|
||||
except Exception:
|
||||
pass
|
||||
if isinstance(val, int):
|
||||
self._is_on = val
|
||||
if val > 0:
|
||||
self._brightness = val
|
||||
else:
|
||||
self._is_on = val == 'ON'
|
||||
|
||||
19
custom_components/mega/manifest.json
Normal file
19
custom_components/mega/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "mega",
|
||||
"name": "mega",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/mega",
|
||||
"requirements": [
|
||||
"beautifulsoup4",
|
||||
"lxml"
|
||||
],
|
||||
"ssdp": [],
|
||||
"zeroconf": [],
|
||||
"homekit": {},
|
||||
"dependencies": [
|
||||
"mqtt"
|
||||
],
|
||||
"codeowners": [
|
||||
"@andvikt"
|
||||
]
|
||||
}
|
||||
179
custom_components/mega/sensor.py
Normal file
179
custom_components/mega/sensor.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""Platform for light integration."""
|
||||
import logging
|
||||
|
||||
import typing
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
PLATFORM_SCHEMA as SENSOR_SCHEMA,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_HUMIDITY
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
CONF_UNIQUE_ID,
|
||||
CONF_ID,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from .entities import BaseMegaEntity
|
||||
from .const import CONF_KEY, TEMP, HUM, W1, W1BUS
|
||||
from .hub import MegaD
|
||||
import re
|
||||
|
||||
lg = logging.getLogger(__name__)
|
||||
TEMP_PATT = re.compile(r'temp:([01234567890\.]+)')
|
||||
HUM_PATT = re.compile(r'hum:([01234567890\.]+)')
|
||||
PATTERNS = {
|
||||
TEMP: TEMP_PATT,
|
||||
HUM: HUM_PATT,
|
||||
}
|
||||
|
||||
UNITS = {
|
||||
TEMP: '°C',
|
||||
HUM: '%'
|
||||
}
|
||||
CLASSES = {
|
||||
TEMP: DEVICE_CLASS_TEMPERATURE,
|
||||
HUM: DEVICE_CLASS_HUMIDITY
|
||||
}
|
||||
# Validation of the user's configuration
|
||||
_ITEM = {
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
vol.Optional(CONF_UNIQUE_ID): str,
|
||||
vol.Required(CONF_TYPE): vol.Any(
|
||||
W1,
|
||||
W1BUS,
|
||||
),
|
||||
vol.Optional(CONF_KEY, default=''): str,
|
||||
}
|
||||
PLATFORM_SCHEMA = SENSOR_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(str, description="mega id"): [_ITEM]
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
config.pop(CONF_PLATFORM)
|
||||
ents = []
|
||||
for mid, _config in config.items():
|
||||
for x in _config:
|
||||
ent = _make_entity(mid, **x)
|
||||
ents.append(ent)
|
||||
add_entities(ents)
|
||||
return True
|
||||
|
||||
|
||||
def _make_entity(mid: str, port: int, conf: dict):
|
||||
key = conf[CONF_KEY]
|
||||
return Mega1WSensor(
|
||||
key=key,
|
||||
mega_id=mid,
|
||||
port=port,
|
||||
patt=PATTERNS.get(key),
|
||||
unit_of_measurement=UNITS.get(key, UNITS[TEMP]), # TODO: make other units, make options in config flow
|
||||
device_class=CLASSES.get(key, CLASSES[TEMP]),
|
||||
id_suffix=key
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, async_add_devices):
|
||||
mid = config_entry.data[CONF_ID]
|
||||
hub: MegaD = hass.data['mega'][mid]
|
||||
devices = []
|
||||
async for port, pty, m in hub.scan_ports():
|
||||
if pty == "3":
|
||||
values = await hub.get_port(port, get_value=True)
|
||||
lg.debug(f'values: %s', values)
|
||||
if values is None:
|
||||
continue
|
||||
if not isinstance(values, dict):
|
||||
values = {None: values}
|
||||
for key in values:
|
||||
hub.lg.debug(f'add sensor {W1}:{key}')
|
||||
sensor = _make_entity(
|
||||
mid=mid,
|
||||
port=port,
|
||||
conf={
|
||||
CONF_TYPE: W1,
|
||||
CONF_KEY: key,
|
||||
})
|
||||
devices.append(sensor)
|
||||
|
||||
async_add_devices(devices)
|
||||
|
||||
|
||||
class Mega1WSensor(BaseMegaEntity):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unit_of_measurement,
|
||||
device_class,
|
||||
patt=None,
|
||||
key=None,
|
||||
*args,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
1-wire sensor entity
|
||||
|
||||
:param key: key to get value from mega's json
|
||||
:param patt: pattern to extract value, must have at least one group that will contain parsed value
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._value = None
|
||||
self.key = key
|
||||
self.patt = patt
|
||||
self._device_class = device_class
|
||||
self._unit_of_measurement = unit_of_measurement
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
|
||||
await super(Mega1WSensor, self).async_added_to_hass()
|
||||
self.mega.sensors.append(self)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
return self._unit_of_measurement
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
if self.key:
|
||||
return super().unique_id + f'_{self.key}'
|
||||
else:
|
||||
return super(Mega1WSensor, self).unique_id
|
||||
|
||||
@property
|
||||
def device_class(self):
|
||||
return self._device_class
|
||||
|
||||
@property
|
||||
def should_poll(self):
|
||||
return False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
if self._value is None and self._state is not None:
|
||||
return self._state.state
|
||||
return self._value
|
||||
|
||||
def _update(self, payload: dict):
|
||||
val = payload.get('value', '')
|
||||
if isinstance(val, str):
|
||||
val = self.patt.findall(val)
|
||||
if val:
|
||||
self._value = val[0]
|
||||
else:
|
||||
self.lg.warning(f'could not parse: {payload}')
|
||||
elif isinstance(val, dict) and self.key is not None:
|
||||
self._value = val.get(self.key)
|
||||
elif isinstance(val, (float, int)):
|
||||
self._value = val
|
||||
else:
|
||||
self.lg.warning(f'could not parse: {payload}')
|
||||
13
custom_components/mega/services.yaml
Normal file
13
custom_components/mega/services.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
|
||||
save:
|
||||
# Description of the service
|
||||
description: Сохраняет текущее состояние портов (?cmd=s)
|
||||
# Different fields that your service accepts
|
||||
fields:
|
||||
# Key of the field
|
||||
mega_id:
|
||||
# Description of the field
|
||||
description: ID меги
|
||||
# Example value that can be passed for this field
|
||||
example: "def"
|
||||
40
custom_components/mega/strings.json
Normal file
40
custom_components/mega/strings.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"title": "mega",
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"id": "[%key:common::config_flow::data::password%]",
|
||||
"mqtt_id": "[%key:common::config_flow::data::mqtt_id%]",
|
||||
"scan_interval": "[%key:common::config_flow::data::mqtt_id%]",
|
||||
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
|
||||
"invert": "[%key:common::config_flow::data::invert%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]",
|
||||
"duplicate_id": "[%key:common::config_flow::error::duplicate_id%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"scan_interval": "[%key:common::config_flow::data::scan_interval%]",
|
||||
"port_to_scan": "[%key:common::config_flow::data::port_to_scan%]",
|
||||
"reload": "[%key:common::config_flow::data::reload%]",
|
||||
"invert": "[%key:common::config_flow::data::invert%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
custom_components/mega/switch.py
Normal file
87
custom_components/mega/switch.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""Platform for light integration."""
|
||||
import json
|
||||
import logging
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.switch import (
|
||||
PLATFORM_SCHEMA as LIGHT_SCHEMA,
|
||||
SwitchEntity,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PLATFORM,
|
||||
CONF_PORT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.restore_state import RestoreEntity
|
||||
from .entities import BaseMegaEntity
|
||||
|
||||
from .hub import MegaD
|
||||
from .const import CONF_DIMMER, CONF_SWITCH
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Validation of the user's configuration
|
||||
_EXTENDED = {
|
||||
vol.Required(CONF_PORT): int,
|
||||
vol.Optional(CONF_NAME): str,
|
||||
}
|
||||
_ITEM = vol.Any(int, _EXTENDED)
|
||||
DIMMER = {vol.Required(CONF_DIMMER): [_ITEM]}
|
||||
SWITCH = {vol.Required(CONF_SWITCH): [_ITEM]}
|
||||
PLATFORM_SCHEMA = LIGHT_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(str, description="mega id"): [_ITEM],
|
||||
},
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
async def async_setup_platform(hass, config, add_entities, discovery_info=None):
|
||||
config.pop(CONF_PLATFORM)
|
||||
ents = []
|
||||
for mid, _config in config.items():
|
||||
mega = hass.data["mega"][mid]
|
||||
for x in _config:
|
||||
if isinstance(x, int):
|
||||
ent = MegaSwitch(hass, mega=mega, port=x)
|
||||
else:
|
||||
ent = MegaSwitch(
|
||||
hass, mega=mega, port=x[CONF_PORT], name=x[CONF_NAME]
|
||||
)
|
||||
ents.append(ent)
|
||||
|
||||
add_entities(ents)
|
||||
return True
|
||||
|
||||
|
||||
class MegaSwitch(SwitchEntity, BaseMegaEntity):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._is_on = None
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
if self._is_on is not None:
|
||||
return self._is_on
|
||||
return self._state == 'ON'
|
||||
|
||||
async def async_turn_on(self, **kwargs) -> None:
|
||||
cmd = 1
|
||||
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
|
||||
self._is_on = True
|
||||
await self.async_update_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs) -> None:
|
||||
|
||||
cmd = "0"
|
||||
|
||||
if await self.mega.send_command(self.port, f"{self.port}:{cmd}"):
|
||||
self._is_on = False
|
||||
await self.async_update_ha_state()
|
||||
|
||||
def _update(self, payload: dict):
|
||||
val = payload.get("value")
|
||||
self._is_on = val == 'ON'
|
||||
27
custom_components/mega/translations/en.json
Normal file
27
custom_components/mega/translations/en.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Device is already configured"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"unknown": "Unexpected error",
|
||||
"duplicate_id": "Duplicate ID"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Host",
|
||||
"password": "Password",
|
||||
"username": "Username",
|
||||
"id": "ID",
|
||||
"mqtt_id": "MQTT id",
|
||||
"scan_interval": "Scan interval (sec) (used for aliveness and sensors)",
|
||||
"port_to_scan": "Port to poll aliveness (needed only if no sensors used)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "mega"
|
||||
}
|
||||
39
custom_components/mega/translations/ru.json
Normal file
39
custom_components/mega/translations/ru.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Уже настроено"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Невозможно подключиться",
|
||||
"invalid_auth": "Неправильный пароль",
|
||||
"unknown": "Неизвестная ошибка",
|
||||
"duplicate_id": "Дубликат ID"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "Хост",
|
||||
"password": "Пароль",
|
||||
"username": "Пользователь",
|
||||
"id": "ID",
|
||||
"mqtt_id": "MQTT id",
|
||||
"scan_interval": "Периодичность обновлений (сек.)",
|
||||
"port_to_scan": "Порт, который сканируется когда нет датчиков"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"scan_interval": "Периодичность обновлений (сек.)",
|
||||
"port_to_scan": "Порт, который сканируется когда нет датчиков",
|
||||
"reload": "Обновить объекты",
|
||||
"invert": "Список портов (через ,) с инвертированной логикой"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "mega"
|
||||
}
|
||||
8
hacs.json
Normal file
8
hacs.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "MegaD",
|
||||
"country": "RU",
|
||||
"domains": ["mega"],
|
||||
"persistent_directory": "userfiles",
|
||||
"iot_class": ["Assumed State", "Local Push"],
|
||||
"render_readme": true
|
||||
}
|
||||
100
readme.md
Normal file
100
readme.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# MegaD HomeAssistant custom component
|
||||
|
||||
Интеграция с [MegaD-2561](https://www.ab-log.ru/smart-house/ethernet/megad-2561)
|
||||
|
||||
## Основные особенности:
|
||||
- Настройка как из yaml так и из веб-интерфейса
|
||||
- При настройки из веба все порты автоматически добавляются как устройства (для обычных релейных выходов создается
|
||||
`light`, для шим - `light` с поддержкой яркости, для цифровых входов `binary_sensor`, для температурных датчиков
|
||||
`sensor`)
|
||||
- Возможность работы с несколькими megad
|
||||
- Обратная связь по mqtt
|
||||
- Команды выполняются друг за другом без конкурентного доступа к ресурсам megad
|
||||
## Устройства
|
||||
Поддерживаются устройства: light, switch, binary_sensor, sensor. light может работать как диммер
|
||||
## Установка
|
||||
В папке config/custom_components выполнить:
|
||||
```shell
|
||||
git clone https://github.com/andvikt/mega.git
|
||||
```
|
||||
Обновление:
|
||||
```shell
|
||||
git pull
|
||||
```
|
||||
Перезагрузить HA
|
||||
## Зависимости
|
||||
Перед использованием необходимо настроить интеграцию mqtt в HomeAssistant
|
||||
|
||||
## Настройка из веб-интерфейса
|
||||
`Настройки` -> `Интеграции` -> `Добавить интеграцию` в поиске ищем mega
|
||||
|
||||
## Пример настройки с помощью yaml:
|
||||
```yaml
|
||||
mega:
|
||||
mega1:
|
||||
host: 192.168.0.14
|
||||
name: hello
|
||||
password: sec
|
||||
mqtt_id: mega # это id в конфиге меги
|
||||
|
||||
light:
|
||||
- platform: mega
|
||||
mega1:
|
||||
switch:
|
||||
- 1 # можно просто перечислить порты
|
||||
- 2
|
||||
- 3
|
||||
dimmer:
|
||||
- port: 7
|
||||
name: hello # можно использовать расширенный вариант с названиями
|
||||
- 9
|
||||
- 10
|
||||
|
||||
binary_sensor:
|
||||
- platform: mega
|
||||
mega1:
|
||||
- port: 16
|
||||
name: sensor1
|
||||
- port: 18
|
||||
name: sensor2
|
||||
|
||||
sensor:
|
||||
- platform: mega
|
||||
mega1:
|
||||
- port: 10
|
||||
name: some temp
|
||||
type: w1
|
||||
key: temp
|
||||
- port: 10
|
||||
name: some hum
|
||||
type: w1
|
||||
key: hum
|
||||
|
||||
switch:
|
||||
- platform: mega
|
||||
mega1:
|
||||
- 11
|
||||
|
||||
```
|
||||
|
||||
## Сервисы
|
||||
Интеграция предоставляет сервис сохранения состояния портов: `mega.save`
|
||||
```yaml
|
||||
action:
|
||||
service: mega.save
|
||||
data:
|
||||
mega_id: def
|
||||
```
|
||||
|
||||
## Состояния
|
||||
Так же каждое устройство megad опрашивается на предмет работоспособности, текущий статус
|
||||
хранится в mega.<id>
|
||||
|
||||
## Отладка
|
||||
Если возникают проблемы, можно включить детальный лог, для этого в конфиг добавить:
|
||||
```yaml
|
||||
logger:
|
||||
default: info
|
||||
logs:
|
||||
custom_components.mega: debug
|
||||
```
|
||||
Reference in New Issue
Block a user