Skip to content

Commit

Permalink
Add config flow and device registry to fritzbox integration (#31240)
Browse files Browse the repository at this point in the history
* add config flow

* fix pylint

* update lib

* Update config_flow.py

* remote devices layer in config

* add default host

* avoid double setups of entities

* remove async_setup_platform

* store entities in hass.data

* pass fritz connection together with config_entry

* fritz connections try no4 (or is it even more)

* fix comments

* add unloading

* fixed comments

* Update config_flow.py

* Update const.py

* Update config_flow.py

* Update __init__.py

* Update config_flow.py

* Update __init__.py

* Update __init__.py

* Update config_flow.py

* Update __init__.py

* Update __init__.py

* Update __init__.py

* Update config_flow.py

* add init tests

* test unloading

* add switch tests

* add sensor tests

* add climate tests

* test target temperature

* mock config to package

* comments

* test binary sensor state

* add config flow tests

* comments

* add missing tests

* minor

* remove string title

* deprecate yaml

* don't change yaml

* get devices async

* minor

* add devices again

* comments fixed

* unique_id fixes

* fix patches

* Fix schema

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
  • Loading branch information
escoand and MartinHjelmare committed Apr 20, 2020
1 parent 2123f6f commit c87ecf0
Show file tree
Hide file tree
Showing 22 changed files with 1,424 additions and 292 deletions.
1 change: 0 additions & 1 deletion .coveragerc
Expand Up @@ -241,7 +241,6 @@ omit =
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
homeassistant/components/fritzbox_netmonitor/sensor.py
homeassistant/components/fronius/sensor.py
Expand Down
142 changes: 83 additions & 59 deletions homeassistant/components/fritzbox/__init__.py
@@ -1,7 +1,8 @@
"""Support for AVM Fritz!Box smarthome devices."""
import logging
import asyncio
import socket

from pyfritzhome import Fritzhome, LoginError
from pyfritzhome import Fritzhome
import voluptuous as vol

from homeassistant.const import (
Expand All @@ -11,80 +12,103 @@
CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP,
)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv

_LOGGER = logging.getLogger(__name__)
from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS

SUPPORTED_DOMAINS = ["binary_sensor", "climate", "switch", "sensor"]

DOMAIN = "fritzbox"

ATTR_STATE_BATTERY_LOW = "battery_low"
ATTR_STATE_DEVICE_LOCKED = "device_locked"
ATTR_STATE_HOLIDAY_MODE = "holiday_mode"
ATTR_STATE_LOCKED = "locked"
ATTR_STATE_SUMMER_MODE = "summer_mode"
ATTR_STATE_WINDOW_OPEN = "window_open"
def ensure_unique_hosts(value):
"""Validate that all configs have a unique host."""
vol.Schema(vol.Unique("duplicate host entries found"))(
[socket.gethostbyname(entry[CONF_HOST]) for entry in value]
)
return value


CONFIG_SCHEMA = vol.Schema(
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICES): vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string,
}
)
],
)
}
)
},
vol.All(
cv.deprecated(DOMAIN),
{
DOMAIN: vol.Schema(
{
vol.Required(CONF_DEVICES): vol.All(
cv.ensure_list,
[
vol.Schema(
{
vol.Required(
CONF_HOST, default=DEFAULT_HOST
): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Required(
CONF_USERNAME, default=DEFAULT_USERNAME
): cv.string,
}
)
],
ensure_unique_hosts,
)
}
)
},
),
extra=vol.ALLOW_EXTRA,
)


def setup(hass, config):
"""Set up the fritzbox component."""

fritz_list = []
async def async_setup(hass, config):
"""Set up the AVM Fritz!Box integration."""
if DOMAIN in config:
for entry_config in config[DOMAIN][CONF_DEVICES]:
hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=entry_config
)
)

configured_devices = config[DOMAIN].get(CONF_DEVICES)
for device in configured_devices:
host = device.get(CONF_HOST)
username = device.get(CONF_USERNAME)
password = device.get(CONF_PASSWORD)
fritzbox = Fritzhome(host=host, user=username, password=password)
try:
fritzbox.login()
_LOGGER.info("Connected to device %s", device)
except LoginError:
_LOGGER.warning("Login to Fritz!Box %s as %s failed", host, username)
continue
return True

fritz_list.append(fritzbox)

if not fritz_list:
_LOGGER.info("No fritzboxes configured")
return False
async def async_setup_entry(hass, entry):
"""Set up the AVM Fritz!Box platforms."""
fritz = Fritzhome(
host=entry.data[CONF_HOST],
user=entry.data[CONF_USERNAME],
password=entry.data[CONF_PASSWORD],
)
await hass.async_add_executor_job(fritz.login)

hass.data[DOMAIN] = fritz_list
hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()})
hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz

def logout_fritzboxes(event):
"""Close all connections to the fritzboxes."""
for fritz in fritz_list:
fritz.logout()
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)

hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes)
def logout_fritzbox(event):
"""Close connections to this fritzbox."""
fritz.logout()

for domain in SUPPORTED_DOMAINS:
discovery.load_platform(hass, domain, DOMAIN, {}, config)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzbox)

return True


async def async_unload_entry(hass, entry):
"""Unloading the AVM Fritz!Box platforms."""
fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id]
await hass.async_add_executor_job(fritz.logout)

unload_ok = all(
await asyncio.gather(
*[
hass.config_entries.async_forward_entry_unload(entry, component)
for component in PLATFORMS
]
)
)
if unload_ok:
hass.data[DOMAIN][CONF_CONNECTIONS].pop(entry.entry_id)

return unload_ok
45 changes: 29 additions & 16 deletions homeassistant/components/fritzbox/binary_sensor.py
@@ -1,27 +1,24 @@
"""Support for Fritzbox binary sensors."""
import logging

import requests

from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_DEVICES

from . import DOMAIN as FRITZBOX_DOMAIN

_LOGGER = logging.getLogger(__name__)
from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Fritzbox binary sensor platform."""
devices = []
fritz_list = hass.data[FRITZBOX_DOMAIN]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox binary sensor from config_entry."""
entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]

for fritz in fritz_list:
device_list = fritz.get_devices()
for device in device_list:
if device.has_alarm:
devices.append(FritzboxBinarySensor(device, fritz))
for device in await hass.async_add_executor_job(fritz.get_devices):
if device.has_alarm and device.ain not in devices:
entities.append(FritzboxBinarySensor(device, fritz))
devices.add(device.ain)

add_entities(devices, True)
async_add_entities(entities, True)


class FritzboxBinarySensor(BinarySensorDevice):
Expand All @@ -32,6 +29,22 @@ def __init__(self, device, fritz):
self._device = device
self._fritz = fritz

@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self.name,
"identifiers": {(FRITZBOX_DOMAIN, self._device.ain)},
"manufacturer": self._device.manufacturer,
"model": self._device.productname,
"sw_version": self._device.fw_version,
}

@property
def unique_id(self):
"""Return the unique ID of the device."""
return self._device.ain

@property
def name(self):
"""Return the name of the entity."""
Expand All @@ -54,5 +67,5 @@ def update(self):
try:
self._device.update()
except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Connection error: %s", ex)
LOGGER.warning("Connection error: %s", ex)
self._fritz.login()
47 changes: 31 additions & 16 deletions homeassistant/components/fritzbox/climate.py
@@ -1,6 +1,4 @@
"""Support for AVM Fritz!Box smarthome thermostate devices."""
import logging

import requests

from homeassistant.components.climate import ClimateDevice
Expand All @@ -16,22 +14,23 @@
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE,
CONF_DEVICES,
PRECISION_HALVES,
TEMP_CELSIUS,
)

from . import (
from .const import (
ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN,
CONF_CONNECTIONS,
DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
)

_LOGGER = logging.getLogger(__name__)

SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE

OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
Expand All @@ -48,18 +47,18 @@
OFF_REPORT_SET_TEMPERATURE = 0.0


def setup_platform(hass, config, add_entities, discovery_info=None):
"""Set up the Fritzbox smarthome thermostat platform."""
devices = []
fritz_list = hass.data[FRITZBOX_DOMAIN]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox smarthome thermostat from config_entry."""
entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]

for fritz in fritz_list:
device_list = fritz.get_devices()
for device in device_list:
if device.has_thermostat:
devices.append(FritzboxThermostat(device, fritz))
for device in await hass.async_add_executor_job(fritz.get_devices):
if device.has_thermostat and device.ain not in devices:
entities.append(FritzboxThermostat(device, fritz))
devices.add(device.ain)

add_entities(devices)
async_add_entities(entities)


class FritzboxThermostat(ClimateDevice):
Expand All @@ -74,6 +73,22 @@ def __init__(self, device, fritz):
self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_temperature

@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self.name,
"identifiers": {(FRITZBOX_DOMAIN, self._device.ain)},
"manufacturer": self._device.manufacturer,
"model": self._device.productname,
"sw_version": self._device.fw_version,
}

@property
def unique_id(self):
"""Return the unique ID of the device."""
return self._device.ain

@property
def supported_features(self):
"""Return the list of supported features."""
Expand Down Expand Up @@ -205,5 +220,5 @@ def update(self):
self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_temperature
except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Fritzbox connection error: %s", ex)
LOGGER.warning("Fritzbox connection error: %s", ex)
self._fritz.login()

0 comments on commit c87ecf0

Please sign in to comment.