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 config flow and device registry to fritzbox integration #31240

Merged
merged 56 commits into from Apr 20, 2020
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
2f85e9d
add config flow
escoand Jan 28, 2020
0dec687
fix pylint
escoand Jan 28, 2020
6414aee
update lib
escoand Jan 28, 2020
3a9c02f
Update config_flow.py
escoand Jan 29, 2020
469213e
remote devices layer in config
escoand Jan 31, 2020
7a5788f
Merge branch 'fritzbox_config_flow' of github.com:escoand/home-assist…
escoand Jan 31, 2020
890504d
add default host
escoand Jan 31, 2020
60e97cc
avoid double setups of entities
escoand Feb 1, 2020
fb67cd9
remove async_setup_platform
escoand Feb 2, 2020
8744184
store entities in hass.data
escoand Feb 3, 2020
999d28a
pass fritz connection together with config_entry
escoand Feb 3, 2020
af95de9
fritz connections try no4 (or is it even more)
escoand Feb 3, 2020
0800e08
fix comments
escoand Feb 4, 2020
cb1ac6e
add unloading
escoand Feb 4, 2020
fbe1fbb
fixed comments
escoand Feb 7, 2020
8bf9283
Update config_flow.py
escoand Feb 13, 2020
76d7112
Update const.py
escoand Feb 17, 2020
8786b38
Update config_flow.py
escoand Feb 17, 2020
64ba364
Update __init__.py
escoand Feb 17, 2020
60278ab
Update config_flow.py
escoand Feb 17, 2020
428ceba
Update __init__.py
escoand Feb 17, 2020
cabe57f
Update __init__.py
escoand Feb 17, 2020
16853b6
Update config_flow.py
escoand Feb 17, 2020
9a1a6cd
Update __init__.py
escoand Feb 17, 2020
a62a725
Update __init__.py
escoand Feb 18, 2020
431f230
Update __init__.py
escoand Feb 18, 2020
48d24e6
Update config_flow.py
escoand Feb 19, 2020
e5304e2
Merge branch 'dev' of github.com:home-assistant/home-assistant into f…
escoand Feb 27, 2020
1447914
add init tests
escoand Feb 27, 2020
47a8206
Merge branch 'fritzbox_config_flow' of github.com:escoand/home-assist…
escoand Feb 27, 2020
8ddf348
test unloading
escoand Mar 12, 2020
995632f
add switch tests
escoand Mar 12, 2020
2cbea8c
add sensor tests
escoand Mar 12, 2020
18fca2a
Merge branch 'dev' into fritzbox_config_flow
escoand Mar 12, 2020
26fb29c
add climate tests
escoand Apr 7, 2020
f15511e
test target temperature
escoand Apr 7, 2020
c39a81a
Merge remote-tracking branch 'upstream/dev' into fritzbox_config_flow
escoand Apr 7, 2020
5187bac
mock config to package
escoand Apr 14, 2020
c8cc9ee
comments
escoand Apr 14, 2020
9f8790d
Merge remote-tracking branch 'upstream/dev' into fritzbox_config_flow
escoand Apr 14, 2020
e8a8341
test binary sensor state
escoand Apr 14, 2020
d09df47
add config flow tests
escoand Apr 15, 2020
149a1f9
comments
escoand Apr 15, 2020
4e58bd8
add missing tests
escoand Apr 16, 2020
7351aa7
minor
escoand Apr 16, 2020
bedd85d
Merge remote-tracking branch 'upstream/dev' into fritzbox_config_flow
escoand Apr 16, 2020
fb76e58
remove string title
escoand Apr 16, 2020
ee282d3
deprecate yaml
escoand Apr 16, 2020
c77410f
don't change yaml
escoand Apr 18, 2020
158c0cb
get devices async
escoand Apr 18, 2020
ec297cf
minor
escoand Apr 18, 2020
d94b588
add devices again
escoand Apr 19, 2020
7dd5027
comments fixed
escoand Apr 19, 2020
ac7a8dc
unique_id fixes
escoand Apr 19, 2020
5a797a0
fix patches
escoand Apr 19, 2020
4b848ff
Fix schema
MartinHjelmare Apr 20, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion .coveragerc
Expand Up @@ -240,7 +240,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
144 changes: 84 additions & 60 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,
}
)
],
)
}
)
},
extra=vol.ALLOW_EXTRA,
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,
)
)
MartinHjelmare marked this conversation as resolved.
Show resolved Hide resolved


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()
escoand marked this conversation as resolved.
Show resolved Hide resolved
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)
escoand marked this conversation as resolved.
Show resolved Hide resolved
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For another PR we could consider consolidating the platform setup, since it's very similar for all platforms. The only things that change are the device .has_* property check and the entity class.

"""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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment as above about consolidation. In another PR we could create a common fritz entity base class that holds the common attributes.

"""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()