Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial version of Vilfo Router integration #31177

Merged
merged 10 commits into from
Feb 12, 2020
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -788,6 +788,9 @@ omit =
homeassistant/components/vesync/switch.py
homeassistant/components/viaggiatreno/sensor.py
homeassistant/components/vicare/*
homeassistant/components/vilfo/__init__.py
homeassistant/components/vilfo/sensor.py
homeassistant/components/vilfo/const.py
homeassistant/components/vivotek/camera.py
homeassistant/components/vlc/media_player.py
homeassistant/components/vlc_telnet/media_player.py
Expand Down
1 change: 1 addition & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,7 @@ homeassistant/components/versasense/* @flamm3blemuff1n
homeassistant/components/version/* @fabaff
homeassistant/components/vesync/* @markperdue @webdjoe
homeassistant/components/vicare/* @oischinger
homeassistant/components/vilfo/* @ManneW
homeassistant/components/vivotek/* @HarlemSquirrel
homeassistant/components/vizio/* @raman325
homeassistant/components/vlc_telnet/* @rodripf
Expand Down
114 changes: 114 additions & 0 deletions homeassistant/components/vilfo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
"""The Vilfo Router integration."""
import asyncio
from datetime import timedelta
import logging

from vilfo import Client as VilfoClient
from vilfo.exceptions import VilfoException

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
from homeassistant.util import Throttle

from .const import DOMAIN, ROUTER_DEFAULT_HOST

PLATFORMS = ["sensor"]

DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)

_LOGGER = logging.getLogger(__name__)


async def async_setup(hass: HomeAssistantType, config: ConfigType):
"""Set up the Vilfo Router component."""
hass.data.setdefault(DOMAIN, {})
return True


async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Vilfo Router from a config entry."""
host = entry.data[CONF_HOST]
access_token = entry.data[CONF_ACCESS_TOKEN]

vilfo_router = VilfoRouterData(host, access_token)

await vilfo_router.async_update()

if not vilfo_router.available:
raise ConfigEntryNotReady

hass.data[DOMAIN][entry.entry_id] = vilfo_router

for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

return True


async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry."""
unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, platform)
for platform in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)

return unload_ok


class VilfoRouterData:
"""Define an object to hold sensor data."""

def __init__(self, host, access_token):
"""Initialize."""
self._vilfo = VilfoClient(host, access_token)
self.host = host
self.available = False
self.firmware_version = None
self.mac_address = self._vilfo.mac
self.data = {}
self._unavailable_logged = False

@property
def unique_id(self):
"""Get the unique_id for the Vilfo Router."""
if self.mac_address:
return self.mac_address

if self.host == ROUTER_DEFAULT_HOST:
return self.host

return self.host
ManneW marked this conversation as resolved.
Show resolved Hide resolved

@Throttle(DEFAULT_SCAN_INTERVAL)
async def async_update(self):
"""Update data using calls to VilfoClient library."""
try:
board_information = self._vilfo.get_board_information()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
self.firmware_version = board_information["version"]
self.data["boot_time"] = board_information["bootTime"]
self.data["load"] = self._vilfo.get_load()
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved

self.available = True
except VilfoException as error:
if not self._unavailable_logged:
_LOGGER.error(
"Could not fetch data from %s, error: %s", self.host, error
)
self._unavailable_logged = True
self.available = False
return

if self.available and self._unavailable_logged:
_LOGGER.info("Vilfo Router %s is available again", self.host)
self._unavailable_logged = False
147 changes: 147 additions & 0 deletions homeassistant/components/vilfo/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"""Config flow for Vilfo Router integration."""
import ipaddress
import logging
import re

from vilfo import Client as VilfoClient
from vilfo.exceptions import (
AuthenticationException as VilfoAuthenticationException,
VilfoException,
)
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC

from .const import DOMAIN # pylint:disable=unused-import
from .const import ROUTER_DEFAULT_HOST

_LOGGER = logging.getLogger(__name__)

DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST, default=ROUTER_DEFAULT_HOST): str,
vol.Required(CONF_ACCESS_TOKEN, default=""): str,
}
)

RESULT_SUCCESS = "success"
RESULT_CANNOT_CONNECT = "cannot_connect"
RESULT_INVALID_AUTH = "invalid_auth"


def host_valid(host):
"""Return True if hostname or IP address is valid."""
try:
if ipaddress.ip_address(host).version == (4 or 6):
return True
except ValueError:
disallowed = re.compile(r"[^a-zA-Z\d\-]")
return all(x and not disallowed.search(x) for x in host.split("."))


def _try_connect_and_fetch_basic_info(host, token):
"""Attempt to connect and call the ping endpoint and, if successful, fetch basic information."""

# Perform the ping. This doesn't validate authentication.
controller = VilfoClient(host=host, token=token)
result = {"type": None, "data": {}}

try:
controller.ping()
except VilfoException:
result["type"] = RESULT_CANNOT_CONNECT
result["data"] = CannotConnect
return result

# Perform a call that requires authentication.
try:
controller.get_board_information()
except VilfoAuthenticationException:
result["type"] = RESULT_INVALID_AUTH
result["data"] = InvalidAuth
return result

if controller.mac:
result["data"][CONF_ID] = controller.mac
result["data"][CONF_MAC] = controller.mac
else:
result["data"][CONF_ID] = host
result["data"][CONF_MAC] = None

result["type"] = RESULT_SUCCESS

return result


async def validate_input(hass: core.HomeAssistant, data):
"""Validate the user input allows us to connect.

Data has the keys from DATA_SCHEMA with values provided by the user.
"""

# Validate the host before doing anything else.
if not host_valid(data[CONF_HOST]):
raise InvalidHost

config = {}

result = await hass.async_add_executor_job(
_try_connect_and_fetch_basic_info, data[CONF_HOST], data[CONF_ACCESS_TOKEN]
)

if result["type"] != RESULT_SUCCESS:
raise result["data"]

# Return some info we want to store in the config entry.
result_data = result["data"]
config["title"] = f"{data[CONF_HOST]}"
config[CONF_MAC] = result_data[CONF_MAC]
config[CONF_HOST] = data[CONF_HOST]
config[CONF_ID] = result_data[CONF_ID]

return config


class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Vilfo Router."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

async def async_step_user(self, user_input=None):
"""Handle the initial step."""
errors = {}
if user_input is not None:
try:
info = await validate_input(self.hass, user_input)
except InvalidHost:
errors[CONF_HOST] = "wrong_host"
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except Exception as err: # pylint: disable=broad-except
_LOGGER.error("Unexpected exception: %s", err)
errors["base"] = "unknown"
else:
await self.async_set_unique_id(info[CONF_ID])
self._abort_if_unique_id_configured()

return self.async_create_entry(title=info["title"], data=user_input)

return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""


class InvalidHost(exceptions.HomeAssistantError):
"""Error to indicate that hostname/IP address is invalid."""
36 changes: 36 additions & 0 deletions homeassistant/components/vilfo/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Constants for the Vilfo Router integration."""
from homeassistant.const import DEVICE_CLASS_TIMESTAMP

DOMAIN = "vilfo"

ATTR_API_DATA_FIELD = "api_data_field"
ATTR_API_DATA_FIELD_LOAD = "load"
ATTR_API_DATA_FIELD_BOOT_TIME = "boot_time"
ATTR_DEVICE_CLASS = "device_class"
ATTR_ICON = "icon"
ATTR_LABEL = "label"
ATTR_LOAD = "load"
ATTR_UNIT = "unit"
ATTR_BOOT_TIME = "boot_time"

ROUTER_DEFAULT_HOST = "admin.vilfo.com"
ROUTER_DEFAULT_MODEL = "Vilfo Router"
ROUTER_DEFAULT_NAME = "Vilfo Router"
ROUTER_MANUFACTURER = "Vilfo AB"

UNIT_PERCENT = "%"

SENSOR_TYPES = {
"load": {
ATTR_LABEL: ATTR_LOAD.replace("_", " ").title(),
ATTR_UNIT: UNIT_PERCENT,
ATTR_ICON: "mdi:memory",
ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_LOAD,
},
"boot_time": {
ATTR_LABEL: ATTR_BOOT_TIME.replace("_", " ").title(),
Quentame marked this conversation as resolved.
Show resolved Hide resolved
ATTR_ICON: "mdi:timer",
ATTR_API_DATA_FIELD: ATTR_API_DATA_FIELD_BOOT_TIME,
ATTR_DEVICE_CLASS: DEVICE_CLASS_TIMESTAMP,
},
}
9 changes: 9 additions & 0 deletions homeassistant/components/vilfo/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"domain": "vilfo",
"name": "Vilfo Router",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/vilfo",
"requirements": ["vilfo-api-client==0.3.2"],
"dependencies": [],
"codeowners": ["@ManneW"]
}
Loading