Skip to content

Commit

Permalink
Refactor Freebox : add config flow + temperature sensor + signal disp…
Browse files Browse the repository at this point in the history
…atch (#30334)

* Add config flow to Freebox

* Add manufacturer in device_tracker info

* Add device_info to sensor + switch

* Add device_info: connections

* Add config_flow test + update .coveragerc

* Typing

* Add device_type icon

* Remove one error log

* Fix pylint

* Add myself as CODEOWNER

* Handle sync in one place

* Separate the Freebox[Router/Device/Sensor] from __init__.py

* Add link step to config flow

* Make temperature sensors auto-discovered

* Use device activity instead of reachablility for device_tracker

* Store token file in .storage

Depending on host if list of Freebox integration on the future without breaking change

* Remove IP sensors + add Freebox router as a device with attrs : IPs, conection type, uptime, version & serial

* Add sensor should_poll=False

* Test typing

* Handle devices with no name

* None is the default for data

* Fix comment

* Use config_entry.unique_id

* Add async_unload_entry with asyncio

* Add and use bunch of data size and rate related constants (#31781)

* Review

* Remove useless "already_configured" error string

* Review : merge 2 device & 2 sensor classes

* Entities from platforms

* Fix unload + add device after setup + clean loggers

* async_add_entities True

* Review

* Use pathlib + refactor get_api

* device_tracker set + tests with CoroutineMock()

* Removing active & reachable from tracker attrs

* Review

* Fix pipeline

* typing

* typing

* typing

* Raise ConfigEntryNotReady when HttpRequestError at setup

* Review

* Multiple Freebox s

* Review: store sensors in router

* Freebox: a sensor story
  • Loading branch information
Quentame committed Mar 11, 2020
1 parent 180bcad commit 19be4a5
Show file tree
Hide file tree
Showing 17 changed files with 922 additions and 170 deletions.
6 changes: 5 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,11 @@ omit =
homeassistant/components/foscam/const.py
homeassistant/components/foursquare/*
homeassistant/components/free_mobile/notify.py
homeassistant/components/freebox/*
homeassistant/components/freebox/__init__.py
homeassistant/components/freebox/device_tracker.py
homeassistant/components/freebox/router.py
homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py
homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritzbox/*
homeassistant/components/fritzbox_callmonitor/sensor.py
Expand Down
2 changes: 1 addition & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ homeassistant/components/fortigate/* @kifeo
homeassistant/components/fortios/* @kimfrellsen
homeassistant/components/foscam/* @skgsergio
homeassistant/components/foursquare/* @robbiet480
homeassistant/components/freebox/* @snoof85
homeassistant/components/freebox/* @snoof85 @Quentame
homeassistant/components/fronius/* @nielstron
homeassistant/components/frontend/* @home-assistant/frontend
homeassistant/components/garmin_connect/* @cyberjunky
Expand Down
26 changes: 26 additions & 0 deletions homeassistant/components/freebox/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"config": {
"abort": {
"already_configured": "Host already configured"
},
"error": {
"connection_failed": "Failed to connect, please try again",
"register_failed": "Failed to register, please try again",
"unknown": "Unknown error: please retry later"
},
"step": {
"link": {
"description": "Click \"Submit\", then touch the right arrow on the router to register Freebox with Home Assistant.\n\n![Location of button on the router](/static/images/config_freebox.png)",
"title": "Link Freebox router"
},
"user": {
"data": {
"host": "Host",
"port": "Port"
},
"title": "Freebox"
}
},
"title": "Freebox"
}
}
109 changes: 61 additions & 48 deletions homeassistant/components/freebox/__init__.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,26 @@
"""Support for Freebox devices (Freebox v6 and Freebox mini 4K)."""
import asyncio
import logging
import socket

from aiofreepybox import Freepybox
from aiofreepybox.exceptions import HttpRequestError
import voluptuous as vol

from homeassistant.components.discovery import SERVICE_FREEBOX
from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers import config_validation as cv, discovery
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.typing import HomeAssistantType

_LOGGER = logging.getLogger(__name__)
from .const import DOMAIN, PLATFORMS
from .router import FreeboxRouter

DOMAIN = "freebox"
DATA_FREEBOX = DOMAIN
_LOGGER = logging.getLogger(__name__)

FREEBOX_CONFIG_FILE = "freebox.conf"
FREEBOX_SCHEMA = vol.Schema(
{vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port}
)

CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port}
)
},
{DOMAIN: vol.Schema(vol.All(cv.ensure_list, [FREEBOX_SCHEMA]))},
extra=vol.ALLOW_EXTRA,
)

Expand All @@ -37,54 +34,70 @@ async def discovery_dispatch(service, discovery_info):
host = discovery_info.get("properties", {}).get("api_domain")
port = discovery_info.get("properties", {}).get("https_port")
_LOGGER.info("Discovered Freebox server: %s:%s", host, port)
await async_setup_freebox(hass, config, host, port)
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN,
context={"source": SOURCE_DISCOVERY},
data={CONF_HOST: host, CONF_PORT: port},
)
)

discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch)

if conf is not None:
host = conf.get(CONF_HOST)
port = conf.get(CONF_PORT)
await async_setup_freebox(hass, config, host, port)
if conf is None:
return True

for freebox_conf in conf:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=freebox_conf,
)
)

return True


async def async_setup_freebox(hass, config, host, port):
"""Start up the Freebox component platforms."""
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry):
"""Set up Freebox component."""
router = FreeboxRouter(hass, entry)
await router.setup()

app_desc = {
"app_id": "hass",
"app_name": "Home Assistant",
"app_version": "0.65",
"device_name": socket.gethostname(),
}
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.unique_id] = router

token_file = hass.config.path(FREEBOX_CONFIG_FILE)
api_version = "v6"
for platform in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, platform)
)

fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version)
# Services
async def async_reboot(call):
"""Handle reboot service call."""
await router.reboot()

try:
await fbx.open(host, port)
except HttpRequestError:
_LOGGER.exception("Failed to connect to Freebox")
else:
hass.data[DATA_FREEBOX] = fbx
hass.services.async_register(DOMAIN, "reboot", async_reboot)

async def async_freebox_reboot(call):
"""Handle reboot service call."""
await fbx.system.reboot()
async def async_close_connection(event):
"""Close Freebox connection on HA Stop."""
await router.close()

hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection)

return True

hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config))
hass.async_create_task(
async_load_platform(hass, "device_tracker", DOMAIN, {}, config)
)
hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config))

async def close_fbx(event):
"""Close Freebox connection on HA Stop."""
await fbx.close()
async def async_unload_entry(hass: HomeAssistantType, 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:
router = hass.data[DOMAIN].pop(entry.unique_id)
await router.close()

hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx)
return unload_ok
110 changes: 110 additions & 0 deletions homeassistant/components/freebox/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"""Config flow to configure the Freebox integration."""
import logging

from aiofreepybox.exceptions import AuthorizationError, HttpRequestError
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT

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

_LOGGER = logging.getLogger(__name__)


class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL

def __init__(self):
"""Initialize Freebox config flow."""
self._host = None
self._port = None

def _show_setup_form(self, user_input=None, errors=None):
"""Show the setup form to the user."""

if user_input is None:
user_input = {}

return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str,
vol.Required(CONF_PORT, default=user_input.get(CONF_PORT, "")): int,
}
),
errors=errors or {},
)

async def async_step_user(self, user_input=None):
"""Handle a flow initiated by the user."""
errors = {}

if user_input is None:
return self._show_setup_form(user_input, errors)

self._host = user_input[CONF_HOST]
self._port = user_input[CONF_PORT]

# Check if already configured
await self.async_set_unique_id(self._host)
self._abort_if_unique_id_configured()

return await self.async_step_link()

async def async_step_link(self, user_input=None):
"""Attempt to link with the Freebox router.
Given a configured host, will ask the user to press the button
to connect to the router.
"""
if user_input is None:
return self.async_show_form(step_id="link")

errors = {}

fbx = await get_api(self.hass, self._host)
try:
# Open connection and check authentication
await fbx.open(self._host, self._port)

# Check permissions
await fbx.system.get_config()
await fbx.lan.get_hosts_list()
await self.hass.async_block_till_done()

# Close connection
await fbx.close()

return self.async_create_entry(
title=self._host, data={CONF_HOST: self._host, CONF_PORT: self._port},
)

except AuthorizationError as error:
_LOGGER.error(error)
errors["base"] = "register_failed"

except HttpRequestError:
_LOGGER.error("Error connecting to the Freebox router at %s", self._host)
errors["base"] = "connection_failed"

except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Unknown error connecting with Freebox router at %s", self._host
)
errors["base"] = "unknown"

return self.async_show_form(step_id="link", errors=errors)

async def async_step_import(self, user_input=None):
"""Import a config entry."""
return await self.async_step_user(user_input)

async def async_step_discovery(self, user_input=None):
"""Initialize step from discovery."""
return await self.async_step_user(user_input)
75 changes: 75 additions & 0 deletions homeassistant/components/freebox/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""Freebox component constants."""
import socket

from homeassistant.const import (
DATA_RATE_KILOBYTES_PER_SECOND,
DEVICE_CLASS_TEMPERATURE,
TEMP_CELSIUS,
)

DOMAIN = "freebox"

APP_DESC = {
"app_id": "hass",
"app_name": "Home Assistant",
"app_version": "0.106",
"device_name": socket.gethostname(),
}
API_VERSION = "v6"

PLATFORMS = ["device_tracker", "sensor", "switch"]

DEFAULT_DEVICE_NAME = "Unknown device"

# to store the cookie
STORAGE_KEY = DOMAIN
STORAGE_VERSION = 1

# Sensor
SENSOR_NAME = "name"
SENSOR_UNIT = "unit"
SENSOR_ICON = "icon"
SENSOR_DEVICE_CLASS = "device_class"

CONNECTION_SENSORS = {
"rate_down": {
SENSOR_NAME: "Freebox download speed",
SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND,
SENSOR_ICON: "mdi:download-network",
SENSOR_DEVICE_CLASS: None,
},
"rate_up": {
SENSOR_NAME: "Freebox upload speed",
SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND,
SENSOR_ICON: "mdi:upload-network",
SENSOR_DEVICE_CLASS: None,
},
}

TEMPERATURE_SENSOR_TEMPLATE = {
SENSOR_NAME: None,
SENSOR_UNIT: TEMP_CELSIUS,
SENSOR_ICON: "mdi:thermometer",
SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
}

# Icons
DEVICE_ICONS = {
"freebox_delta": "mdi:television-guide",
"freebox_hd": "mdi:television-guide",
"freebox_mini": "mdi:television-guide",
"freebox_player": "mdi:television-guide",
"ip_camera": "mdi:cctv",
"ip_phone": "mdi:phone-voip",
"laptop": "mdi:laptop",
"multimedia_device": "mdi:play-network",
"nas": "mdi:nas",
"networking_device": "mdi:network",
"printer": "mdi:printer",
"router": "mdi:router-wireless",
"smartphone": "mdi:cellphone",
"tablet": "mdi:tablet",
"television": "mdi:television",
"vg_console": "mdi:gamepad-variant",
"workstation": "mdi:desktop-tower-monitor",
}
Loading

0 comments on commit 19be4a5

Please sign in to comment.