Skip to content

Commit

Permalink
Rework Mikrotik device scanning following Unifi (home-assistant#27484)
Browse files Browse the repository at this point in the history
* rework device scanning, add tests

* update requirements and coverage

* fix description comments

* update tests, fix disabled entity updates

* rework device scanning, add tests

* update requirements and coverage

* fix description comments

* update tests, fix disabled entity updates

* update librouteros to 3.0.0

* fix sorting

* fix sorting 2

* revert to 2.3.0 as 3.0.0 requires code update

* fix requirements

* apply fixes

* fix tests

* update hub.py and fix tests

* fix test_hub_setup_failed

* rebased on dev and update librouteros to 3.0.0

* fixed test_config_flow

* fixed tests

* fix test_config_flow
  • Loading branch information
engrbm87 authored Jan 30, 2020
1 parent 33361f8 commit 9432054
Show file tree
Hide file tree
Showing 18 changed files with 1,556 additions and 351 deletions.
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@ omit =
homeassistant/components/metoffice/weather.py
homeassistant/components/microsoft/tts.py
homeassistant/components/miflora/sensor.py
homeassistant/components/mikrotik/*
homeassistant/components/mikrotik/hub.py
homeassistant/components/mikrotik/device_tracker.py
homeassistant/components/mill/climate.py
homeassistant/components/mill/const.py
homeassistant/components/minio/*
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ homeassistant/components/met/* @danielhiversen
homeassistant/components/meteo_france/* @victorcerutti @oncleben31
homeassistant/components/meteoalarm/* @rolfberkenbosch
homeassistant/components/miflora/* @danielhiversen @ChristianKuehnel
homeassistant/components/mikrotik/* @engrbm87
homeassistant/components/mill/* @danielhiversen
homeassistant/components/min_max/* @fabaff
homeassistant/components/minio/* @tkislan
Expand Down
37 changes: 37 additions & 0 deletions homeassistant/components/mikrotik/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"config": {
"title": "Mikrotik",
"step": {
"user": {
"title": "Set up Mikrotik Router",
"data": {
"name": "Name",
"host": "Host",
"username": "Username",
"password": "Password",
"port": "Port",
"verify_ssl": "Use ssl"
}
}
},
"error": {
"name_exists": "Name exists",
"cannot_connect": "Connection Unsuccessful",
"wrong_credentials": "Wrong Credentials"
},
"abort": {
"already_configured": "Mikrotik is already configured"
}
},
"options": {
"step": {
"device_tracker": {
"data": {
"arp_ping": "Enable ARP ping",
"force_dhcp": "Force scanning using DHCP",
"detection_time": "Consider home interval"
}
}
}
}
}
197 changes: 52 additions & 145 deletions homeassistant/components/mikrotik/__init__.py
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
"""The mikrotik component."""
import logging
import ssl

from librouteros import connect
from librouteros.exceptions import LibRouterosError
from librouteros.login import plain as login_plain, token as login_token
"""The Mikrotik component."""
import voluptuous as vol

from homeassistant.components.device_tracker import DOMAIN as DEVICE_TRACKER
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import (
CONF_HOST,
CONF_METHOD,
CONF_NAME,
CONF_PASSWORD,
CONF_PORT,
CONF_SSL,
CONF_USERNAME,
CONF_VERIFY_SSL,
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.discovery import load_platform

from .const import (
ATTR_MANUFACTURER,
CONF_ARP_PING,
CONF_ENCODING,
CONF_LOGIN_METHOD,
CONF_TRACK_DEVICES,
DEFAULT_ENCODING,
CONF_DETECTION_TIME,
CONF_FORCE_DHCP,
DEFAULT_API_PORT,
DEFAULT_DETECTION_TIME,
DEFAULT_NAME,
DOMAIN,
HOSTS,
IDENTITY,
MIKROTIK_SERVICES,
MTK_LOGIN_PLAIN,
MTK_LOGIN_TOKEN,
NAME,
)

_LOGGER = logging.getLogger(__name__)

MTK_DEFAULT_API_PORT = "8728"
MTK_DEFAULT_API_SSL_PORT = "8729"
from .hub import MikrotikHub

MIKROTIK_SCHEMA = vol.All(
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_METHOD): cv.string,
vol.Optional(CONF_LOGIN_METHOD): vol.Any(MTK_LOGIN_PLAIN, MTK_LOGIN_TOKEN),
vol.Optional(CONF_PORT): cv.port,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_ENCODING, default=DEFAULT_ENCODING): cv.string,
vol.Optional(CONF_TRACK_DEVICES, default=True): cv.boolean,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_PORT, default=DEFAULT_API_PORT): cv.port,
vol.Optional(CONF_VERIFY_SSL, default=False): cv.boolean,
vol.Optional(CONF_ARP_PING, default=False): cv.boolean,
vol.Optional(CONF_FORCE_DHCP, default=False): cv.boolean,
vol.Optional(
CONF_DETECTION_TIME, default=DEFAULT_DETECTION_TIME
): cv.time_period,
}
)
)
Expand All @@ -61,124 +47,45 @@
)


def setup(hass, config):
"""Set up the Mikrotik component."""
hass.data[DOMAIN] = {HOSTS: {}}

for device in config[DOMAIN]:
host = device[CONF_HOST]
use_ssl = device.get(CONF_SSL)
user = device.get(CONF_USERNAME)
password = device.get(CONF_PASSWORD, "")
login = device.get(CONF_LOGIN_METHOD)
encoding = device.get(CONF_ENCODING)
track_devices = device.get(CONF_TRACK_DEVICES)

if CONF_PORT in device:
port = device.get(CONF_PORT)
else:
if use_ssl:
port = MTK_DEFAULT_API_SSL_PORT
else:
port = MTK_DEFAULT_API_PORT

if login == MTK_LOGIN_PLAIN:
login_method = login_plain
else:
login_method = login_token

try:
api = MikrotikClient(
host, use_ssl, port, user, password, login_method, encoding
async def async_setup(hass, config):
"""Import the Mikrotik component from config."""

if DOMAIN in config:
for entry in config[DOMAIN]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=entry
)
)
api.connect_to_device()
hass.data[DOMAIN][HOSTS][host] = {"config": device, "api": api}
except LibRouterosError as api_error:
_LOGGER.error("Mikrotik %s error %s", host, api_error)
continue

if track_devices:
hass.data[DOMAIN][HOSTS][host][DEVICE_TRACKER] = True
load_platform(hass, DEVICE_TRACKER, DOMAIN, None, config)
return True


async def async_setup_entry(hass, config_entry):
"""Set up the Mikrotik component."""

if not hass.data[DOMAIN][HOSTS]:
hub = MikrotikHub(hass, config_entry)
if not await hub.async_setup():
return False

hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = hub
device_registry = await hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
connections={(DOMAIN, hub.serial_num)},
manufacturer=ATTR_MANUFACTURER,
model=hub.model,
name=hub.hostname,
sw_version=hub.firmware,
)

return True


class MikrotikClient:
"""Handle all communication with the Mikrotik API."""

def __init__(self, host, use_ssl, port, user, password, login_method, encoding):
"""Initialize the Mikrotik Client."""
self._host = host
self._use_ssl = use_ssl
self._port = port
self._user = user
self._password = password
self._login_method = login_method
self._encoding = encoding
self._ssl_wrapper = None
self.hostname = None
self._client = None
self._connected = False

def connect_to_device(self):
"""Connect to Mikrotik device."""
self._connected = False
_LOGGER.debug("[%s] Connecting to Mikrotik device", self._host)

kwargs = {
"encoding": self._encoding,
"login_methods": self._login_method,
"port": self._port,
}
async def async_unload_entry(hass, config_entry):
"""Unload a config entry."""
await hass.config_entries.async_forward_entry_unload(config_entry, "device_tracker")

if self._use_ssl:
if self._ssl_wrapper is None:
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
self._ssl_wrapper = ssl_context.wrap_socket
kwargs["ssl_wrapper"] = self._ssl_wrapper

try:
self._client = connect(self._host, self._user, self._password, **kwargs)
self._connected = True
except LibRouterosError as api_error:
_LOGGER.error("Mikrotik %s: %s", self._host, api_error)
self._client = None
return False

self.hostname = self.get_hostname()
_LOGGER.info("Mikrotik Connected to %s (%s)", self.hostname, self._host)
return self._connected

def get_hostname(self):
"""Return device host name."""
data = list(self.command(MIKROTIK_SERVICES[IDENTITY]))
return data[0][NAME] if data else None

def connected(self):
"""Return connected boolean."""
return self._connected

def command(self, cmd, params=None):
"""Retrieve data from Mikrotik API."""
if not self._connected or not self._client:
if not self.connect_to_device():
return None
try:
if params:
response = self._client(cmd=cmd, **params)
else:
response = self._client(cmd=cmd)
except LibRouterosError as api_error:
_LOGGER.error(
"Mikrotik %s failed to retrieve data. cmd=[%s] Error: %s",
self._host,
cmd,
api_error,
)
return None
return response if response else None
hass.data[DOMAIN].pop(config_entry.entry_id)

return True
Loading

0 comments on commit 9432054

Please sign in to comment.