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

Refactor Freebox : add config flow + temperature sensor + signal dispatch #30334

Merged
merged 45 commits into from
Mar 11, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
333b365
Add config flow to Freebox
Quentame Dec 31, 2019
7f0be6d
Add manufacturer in device_tracker info
Quentame Jan 2, 2020
fbf4de9
Add device_info to sensor + switch
Quentame Jan 2, 2020
16fcbd4
Add device_info: connections
Quentame Jan 2, 2020
eb542d8
Add config_flow test + update .coveragerc
Quentame Jan 5, 2020
1b8eb6f
Typing
Quentame Jan 5, 2020
c16e63b
Add device_type icon
Quentame Jan 5, 2020
6cbdd20
Remove one error log
Quentame Jan 5, 2020
2225da6
Fix pylint
Quentame Jan 5, 2020
28086a3
Add myself as CODEOWNER
Quentame Jan 5, 2020
2222fac
Handle sync in one place
Quentame Jan 6, 2020
3452eb7
Separate the Freebox[Router/Device/Sensor] from __init__.py
Quentame Jan 9, 2020
c8a7930
Make temperature sensors auto-discovered
Quentame Jan 10, 2020
c3a8dd6
Use device activity instead of reachablility for device_tracker
Quentame Jan 10, 2020
d970b7a
Add link step to config flow
Quentame Jan 9, 2020
5b02613
Fix comment
Quentame Feb 4, 2020
b223056
Store token file in .storage
Quentame Jan 11, 2020
af4722c
Remove IP sensors + add Freebox router as a device with attrs : IPs, …
Quentame Jan 11, 2020
1941916
Add sensor should_poll=False
Quentame Jan 11, 2020
2b02a1f
Use config_entry.unique_id
Quentame Feb 12, 2020
0223084
Test typing
Quentame Jan 20, 2020
8a420cf
Handle devices with no name
Quentame Jan 27, 2020
1dd3f02
None is the default for data
Quentame Feb 4, 2020
8c56ac1
Add async_unload_entry with asyncio
Quentame Feb 12, 2020
b36d8dd
Add and use bunch of data size and rate related constants (#31781)
Quentame Feb 14, 2020
746105c
Review
Quentame Feb 19, 2020
b48cf9d
Remove useless "already_configured" error string
Quentame Feb 19, 2020
90716ed
Review : merge 2 device & 2 sensor classes
Quentame Feb 20, 2020
1bb3e57
Entities from platforms
Quentame Feb 24, 2020
fd5091f
Fix unload + add device after setup + clean loggers
Quentame Feb 25, 2020
41b1aac
async_add_entities True
Quentame Feb 25, 2020
6c19898
Review
Quentame Feb 25, 2020
275d221
Use pathlib + refactor get_api
Quentame Feb 25, 2020
98ebe6d
device_tracker set + tests with CoroutineMock()
Quentame Feb 26, 2020
f9fd400
Removing active & reachable from tracker attrs
Quentame Feb 26, 2020
6a20db7
Review
Quentame Feb 26, 2020
4b2bd31
Fix pipeline
Quentame Feb 26, 2020
53789be
typing
Quentame Feb 26, 2020
0b2e7a4
typing
Quentame Feb 27, 2020
a1396c1
typing
Quentame Feb 27, 2020
a89d3b5
Raise ConfigEntryNotReady when HttpRequestError at setup
Quentame Feb 29, 2020
b22c2e3
Review
Quentame Mar 9, 2020
fa3f939
Multiple Freebox s
Quentame Mar 9, 2020
b3d67bf
Review: store sensors in router
Quentame Mar 11, 2020
ae6223c
Freebox: a sensor story
Quentame Mar 11, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,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 @@ -120,7 +120,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)
Quentame marked this conversation as resolved.
Show resolved Hide resolved

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)
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved
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},
Quentame marked this conversation as resolved.
Show resolved Hide resolved
)

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