Add config flow and device registry to fritzbox integration (#31240)

* 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>
This commit is contained in:
escoand 2020-04-20 15:00:07 +02:00 committed by GitHub
parent 2123f6f133
commit c87ecf0ff6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1403 additions and 271 deletions

View File

@ -241,7 +241,6 @@ omit =
homeassistant/components/freebox/sensor.py homeassistant/components/freebox/sensor.py
homeassistant/components/freebox/switch.py homeassistant/components/freebox/switch.py
homeassistant/components/fritz/device_tracker.py homeassistant/components/fritz/device_tracker.py
homeassistant/components/fritzbox/*
homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_callmonitor/sensor.py
homeassistant/components/fritzbox_netmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py
homeassistant/components/fronius/sensor.py homeassistant/components/fronius/sensor.py

View File

@ -1,7 +1,8 @@
"""Support for AVM Fritz!Box smarthome devices.""" """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 import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
@ -11,24 +12,22 @@ from homeassistant.const import (
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv 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" def ensure_unique_hosts(value):
"""Validate that all configs have a unique host."""
ATTR_STATE_BATTERY_LOW = "battery_low" vol.Schema(vol.Unique("duplicate host entries found"))(
ATTR_STATE_DEVICE_LOCKED = "device_locked" [socket.gethostbyname(entry[CONF_HOST]) for entry in value]
ATTR_STATE_HOLIDAY_MODE = "holiday_mode" )
ATTR_STATE_LOCKED = "locked" return value
ATTR_STATE_SUMMER_MODE = "summer_mode"
ATTR_STATE_WINDOW_OPEN = "window_open"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
vol.All(
cv.deprecated(DOMAIN),
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
{ {
@ -37,54 +36,79 @@ CONFIG_SCHEMA = vol.Schema(
[ [
vol.Schema( vol.Schema(
{ {
vol.Required(CONF_HOST): cv.string, vol.Required(
CONF_HOST, default=DEFAULT_HOST
): cv.string,
vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_PASSWORD): cv.string,
vol.Required(CONF_USERNAME): cv.string, vol.Required(
CONF_USERNAME, default=DEFAULT_USERNAME
): cv.string,
} }
) )
], ],
ensure_unique_hosts,
) )
} }
) )
}, },
),
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
def setup(hass, config): async def async_setup(hass, config):
"""Set up the fritzbox component.""" """Set up the AVM Fritz!Box integration."""
if DOMAIN in config:
fritz_list = [] for entry_config in config[DOMAIN][CONF_DEVICES]:
hass.async_create_task(
configured_devices = config[DOMAIN].get(CONF_DEVICES) hass.config_entries.flow.async_init(
for device in configured_devices: DOMAIN, context={"source": "import"}, data=entry_config
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
fritz_list.append(fritzbox)
if not fritz_list:
_LOGGER.info("No fritzboxes configured")
return False
hass.data[DOMAIN] = fritz_list
def logout_fritzboxes(event):
"""Close all connections to the fritzboxes."""
for fritz in fritz_list:
fritz.logout()
hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, logout_fritzboxes)
for domain in SUPPORTED_DOMAINS:
discovery.load_platform(hass, domain, DOMAIN, {}, config)
return True return True
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.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()})
hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz
for component in PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, component)
)
def logout_fritzbox(event):
"""Close connections to this fritzbox."""
fritz.logout()
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

View File

@ -1,27 +1,24 @@
"""Support for Fritzbox binary sensors.""" """Support for Fritzbox binary sensors."""
import logging
import requests import requests
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import CONF_DEVICES
from . import DOMAIN as FRITZBOX_DOMAIN from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER
_LOGGER = logging.getLogger(__name__)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox binary sensor platform.""" """Set up the Fritzbox binary sensor from config_entry."""
devices = [] entities = []
fritz_list = hass.data[FRITZBOX_DOMAIN] devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for fritz in fritz_list: for device in await hass.async_add_executor_job(fritz.get_devices):
device_list = fritz.get_devices() if device.has_alarm and device.ain not in devices:
for device in device_list: entities.append(FritzboxBinarySensor(device, fritz))
if device.has_alarm: devices.add(device.ain)
devices.append(FritzboxBinarySensor(device, fritz))
add_entities(devices, True) async_add_entities(entities, True)
class FritzboxBinarySensor(BinarySensorDevice): class FritzboxBinarySensor(BinarySensorDevice):
@ -32,6 +29,22 @@ class FritzboxBinarySensor(BinarySensorDevice):
self._device = device self._device = device
self._fritz = fritz 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 @property
def name(self): def name(self):
"""Return the name of the entity.""" """Return the name of the entity."""
@ -54,5 +67,5 @@ class FritzboxBinarySensor(BinarySensorDevice):
try: try:
self._device.update() self._device.update()
except requests.exceptions.HTTPError as ex: except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Connection error: %s", ex) LOGGER.warning("Connection error: %s", ex)
self._fritz.login() self._fritz.login()

View File

@ -1,6 +1,4 @@
"""Support for AVM Fritz!Box smarthome thermostate devices.""" """Support for AVM Fritz!Box smarthome thermostate devices."""
import logging
import requests import requests
from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate import ClimateDevice
@ -16,22 +14,23 @@ from homeassistant.components.climate.const import (
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_DEVICES,
PRECISION_HALVES, PRECISION_HALVES,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from . import ( from .const import (
ATTR_STATE_BATTERY_LOW, ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_HOLIDAY_MODE, ATTR_STATE_HOLIDAY_MODE,
ATTR_STATE_LOCKED, ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE, ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN, ATTR_STATE_WINDOW_OPEN,
CONF_CONNECTIONS,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
) )
_LOGGER = logging.getLogger(__name__)
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF] OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF]
@ -48,18 +47,18 @@ ON_REPORT_SET_TEMPERATURE = 30.0
OFF_REPORT_SET_TEMPERATURE = 0.0 OFF_REPORT_SET_TEMPERATURE = 0.0
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox smarthome thermostat platform.""" """Set up the Fritzbox smarthome thermostat from config_entry."""
devices = [] entities = []
fritz_list = hass.data[FRITZBOX_DOMAIN] devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for fritz in fritz_list: for device in await hass.async_add_executor_job(fritz.get_devices):
device_list = fritz.get_devices() if device.has_thermostat and device.ain not in devices:
for device in device_list: entities.append(FritzboxThermostat(device, fritz))
if device.has_thermostat: devices.add(device.ain)
devices.append(FritzboxThermostat(device, fritz))
add_entities(devices) async_add_entities(entities)
class FritzboxThermostat(ClimateDevice): class FritzboxThermostat(ClimateDevice):
@ -74,6 +73,22 @@ class FritzboxThermostat(ClimateDevice):
self._comfort_temperature = self._device.comfort_temperature self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_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 @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
@ -205,5 +220,5 @@ class FritzboxThermostat(ClimateDevice):
self._comfort_temperature = self._device.comfort_temperature self._comfort_temperature = self._device.comfort_temperature
self._eco_temperature = self._device.eco_temperature self._eco_temperature = self._device.eco_temperature
except requests.exceptions.HTTPError as ex: except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Fritzbox connection error: %s", ex) LOGGER.warning("Fritzbox connection error: %s", ex)
self._fritz.login() self._fritz.login()

View File

@ -0,0 +1,151 @@
"""Config flow for AVM Fritz!Box."""
from urllib.parse import urlparse
from pyfritzhome import Fritzhome, LoginError
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME
# pylint:disable=unused-import
from .const import DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN
DATA_SCHEMA_USER = vol.Schema(
{
vol.Required(CONF_HOST, default=DEFAULT_HOST): str,
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
DATA_SCHEMA_CONFIRM = vol.Schema(
{
vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
RESULT_AUTH_FAILED = "auth_failed"
RESULT_NOT_FOUND = "not_found"
RESULT_SUCCESS = "success"
class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a AVM Fritz!Box config flow."""
VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL
# pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167
def __init__(self):
"""Initialize flow."""
self._host = None
self._manufacturer = None
self._model = None
self._name = None
self._password = None
self._username = None
def _get_entry(self):
return self.async_create_entry(
title=self._name,
data={
CONF_HOST: self._host,
CONF_PASSWORD: self._password,
CONF_USERNAME: self._username,
},
)
def _try_connect(self):
"""Try to connect and check auth."""
fritzbox = Fritzhome(
host=self._host, user=self._username, password=self._password
)
try:
fritzbox.login()
fritzbox.logout()
return RESULT_SUCCESS
except OSError:
return RESULT_NOT_FOUND
except LoginError:
return RESULT_AUTH_FAILED
async def async_step_import(self, user_input=None):
"""Handle configuration by yaml file."""
return await self.async_step_user(user_input)
async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
errors = {}
if user_input is not None:
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data[CONF_HOST] == user_input[CONF_HOST]:
if entry.data != user_input:
self.hass.config_entries.async_update_entry(
entry, data=user_input
)
return self.async_abort(reason="already_configured")
self._host = user_input[CONF_HOST]
self._name = user_input[CONF_HOST]
self._password = user_input[CONF_PASSWORD]
self._username = user_input[CONF_USERNAME]
result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS:
return self._get_entry()
if result != RESULT_AUTH_FAILED:
return self.async_abort(reason=result)
errors["base"] = result
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA_USER, errors=errors
)
async def async_step_ssdp(self, user_input):
"""Handle a flow initialized by discovery."""
host = urlparse(user_input[ATTR_SSDP_LOCATION]).hostname
self.context[CONF_HOST] = host
for progress in self._async_in_progress():
if progress.get("context", {}).get(CONF_HOST) == host:
return self.async_abort(reason="already_in_progress")
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data[CONF_HOST] == host:
if entry.data != user_input:
self.hass.config_entries.async_update_entry(entry, data=user_input)
return self.async_abort(reason="already_configured")
self._host = host
self._name = user_input[ATTR_UPNP_FRIENDLY_NAME]
self.context["title_placeholders"] = {"name": self._name}
return await self.async_step_confirm()
async def async_step_confirm(self, user_input=None):
"""Handle user-confirmation of discovered node."""
errors = {}
if user_input is not None:
self._password = user_input[CONF_PASSWORD]
self._username = user_input[CONF_USERNAME]
result = await self.hass.async_add_executor_job(self._try_connect)
if result == RESULT_SUCCESS:
return self._get_entry()
if result != RESULT_AUTH_FAILED:
return self.async_abort(reason=result)
errors["base"] = result
return self.async_show_form(
step_id="confirm",
data_schema=DATA_SCHEMA_CONFIRM,
description_placeholders={"name": self._name},
errors=errors,
)

View File

@ -0,0 +1,25 @@
"""Constants for the AVM Fritz!Box integration."""
import logging
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"
ATTR_TEMPERATURE_UNIT = "temperature_unit"
ATTR_TOTAL_CONSUMPTION = "total_consumption"
ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit"
CONF_CONNECTIONS = "connections"
DEFAULT_HOST = "fritz.box"
DEFAULT_USERNAME = "admin"
DOMAIN = "fritzbox"
LOGGER = logging.getLogger(__package__)
PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"]

View File

@ -2,6 +2,13 @@
"domain": "fritzbox", "domain": "fritzbox",
"name": "AVM FRITZ!Box", "name": "AVM FRITZ!Box",
"documentation": "https://www.home-assistant.io/integrations/fritzbox", "documentation": "https://www.home-assistant.io/integrations/fritzbox",
"requirements": ["pyfritzhome==0.4.0"], "requirements": ["pyfritzhome==0.4.2"],
"codeowners": [] "ssdp": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"
}
],
"dependencies": [],
"codeowners": [],
"config_flow": true
} }

View File

@ -1,33 +1,35 @@
"""Support for AVM Fritz!Box smarthome temperature sensor only devices.""" """Support for AVM Fritz!Box smarthome temperature sensor only devices."""
import logging
import requests import requests
from homeassistant.const import TEMP_CELSIUS from homeassistant.const import CONF_DEVICES, TEMP_CELSIUS
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN from .const import (
ATTR_STATE_DEVICE_LOCKED,
_LOGGER = logging.getLogger(__name__) ATTR_STATE_LOCKED,
CONF_CONNECTIONS,
DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
)
def setup_platform(hass, config, add_entities, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox smarthome sensor platform.""" """Set up the Fritzbox smarthome sensor from config_entry."""
_LOGGER.debug("Initializing fritzbox temperature sensors") entities = []
devices = [] devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz_list = hass.data[FRITZBOX_DOMAIN] fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for fritz in fritz_list: for device in await hass.async_add_executor_job(fritz.get_devices):
device_list = fritz.get_devices()
for device in device_list:
if ( if (
device.has_temperature_sensor device.has_temperature_sensor
and not device.has_switch and not device.has_switch
and not device.has_thermostat and not device.has_thermostat
and device.ain not in devices
): ):
devices.append(FritzBoxTempSensor(device, fritz)) entities.append(FritzBoxTempSensor(device, fritz))
devices.add(device.ain)
add_entities(devices) async_add_entities(entities)
class FritzBoxTempSensor(Entity): class FritzBoxTempSensor(Entity):
@ -38,6 +40,22 @@ class FritzBoxTempSensor(Entity):
self._device = device self._device = device
self._fritz = fritz 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 @property
def name(self): def name(self):
"""Return the name of the device.""" """Return the name of the device."""
@ -58,7 +76,7 @@ class FritzBoxTempSensor(Entity):
try: try:
self._device.update() self._device.update()
except requests.exceptions.HTTPError as ex: except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Fritzhome connection error: %s", ex) LOGGER.warning("Fritzhome connection error: %s", ex)
self._fritz.login() self._fritz.login()
@property @property

View File

@ -0,0 +1,32 @@
{
"config": {
"flow_title": "AVM FRITZ!Box: {name}",
"step": {
"user": {
"title": "AVM FRITZ!Box",
"description": "Enter your AVM FRITZ!Box information.",
"data": {
"host": "Host or IP address",
"username": "Username",
"password": "Password"
}
},
"confirm": {
"title": "AVM FRITZ!Box",
"description": "Do you want to set up {name}?",
"data": {
"username": "Username",
"password": "Password"
}
}
},
"abort": {
"already_in_progress": "AVM FRITZ!Box configuration is already in progress.",
"already_configured": "This AVM FRITZ!Box is already configured.",
"not_found": "No supported AVM FRITZ!Box found on the network."
},
"error": {
"auth_failed": "Username and/or password are incorrect."
}
}
}

View File

@ -1,34 +1,40 @@
"""Support for AVM Fritz!Box smarthome switch devices.""" """Support for AVM Fritz!Box smarthome switch devices."""
import logging
import requests import requests
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.const import ATTR_TEMPERATURE, ENERGY_KILO_WATT_HOUR, TEMP_CELSIUS from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_DEVICES,
ENERGY_KILO_WATT_HOUR,
TEMP_CELSIUS,
)
from . import ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_LOCKED, DOMAIN as FRITZBOX_DOMAIN from .const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
ATTR_TEMPERATURE_UNIT,
ATTR_TOTAL_CONSUMPTION,
ATTR_TOTAL_CONSUMPTION_UNIT,
CONF_CONNECTIONS,
DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
)
_LOGGER = logging.getLogger(__name__)
ATTR_TOTAL_CONSUMPTION = "total_consumption"
ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit"
ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR
ATTR_TEMPERATURE_UNIT = "temperature_unit"
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Fritzbox smarthome switch from config_entry."""
entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
def setup_platform(hass, config, add_entities, discovery_info=None): for device in await hass.async_add_executor_job(fritz.get_devices):
"""Set up the Fritzbox smarthome switch platform.""" if device.has_switch and device.ain not in devices:
devices = [] entities.append(FritzboxSwitch(device, fritz))
fritz_list = hass.data[FRITZBOX_DOMAIN] devices.add(device.ain)
for fritz in fritz_list: async_add_entities(entities)
device_list = fritz.get_devices()
for device in device_list:
if device.has_switch:
devices.append(FritzboxSwitch(device, fritz))
add_entities(devices)
class FritzboxSwitch(SwitchDevice): class FritzboxSwitch(SwitchDevice):
@ -39,6 +45,22 @@ class FritzboxSwitch(SwitchDevice):
self._device = device self._device = device
self._fritz = fritz 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 @property
def available(self): def available(self):
"""Return if switch is available.""" """Return if switch is available."""
@ -67,7 +89,7 @@ class FritzboxSwitch(SwitchDevice):
try: try:
self._device.update() self._device.update()
except requests.exceptions.HTTPError as ex: except requests.exceptions.HTTPError as ex:
_LOGGER.warning("Fritzhome connection error: %s", ex) LOGGER.warning("Fritzhome connection error: %s", ex)
self._fritz.login() self._fritz.login()
@property @property

View File

@ -35,6 +35,7 @@ FLOWS = [
"flume", "flume",
"flunearyou", "flunearyou",
"freebox", "freebox",
"fritzbox",
"garmin_connect", "garmin_connect",
"gdacs", "gdacs",
"geofency", "geofency",

View File

@ -17,6 +17,11 @@ SSDP = {
"manufacturer": "DIRECTV" "manufacturer": "DIRECTV"
} }
], ],
"fritzbox": [
{
"st": "urn:schemas-upnp-org:device:fritzbox:1"
}
],
"harmony": [ "harmony": [
{ {
"deviceType": "urn:myharmony-com:device:harmony:1", "deviceType": "urn:myharmony-com:device:harmony:1",

View File

@ -1298,7 +1298,7 @@ pyflunearyou==1.0.7
pyfnip==0.2 pyfnip==0.2
# homeassistant.components.fritzbox # homeassistant.components.fritzbox
pyfritzhome==0.4.0 pyfritzhome==0.4.2
# homeassistant.components.fronius # homeassistant.components.fronius
pyfronius==0.4.6 pyfronius==0.4.6

View File

@ -511,7 +511,7 @@ pyflume==0.4.0
pyflunearyou==1.0.7 pyflunearyou==1.0.7
# homeassistant.components.fritzbox # homeassistant.components.fritzbox
pyfritzhome==0.4.0 pyfritzhome==0.4.2
# homeassistant.components.ifttt # homeassistant.components.ifttt
pyfttt==0.3 pyfttt==0.3

View File

@ -1 +1,99 @@
"""Tests for the FritzBox! integration.""" """Tests for the AVM Fritz!Box integration."""
from unittest.mock import Mock
from homeassistant.components.fritzbox.const import DOMAIN
from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
MOCK_CONFIG = {
DOMAIN: {
CONF_DEVICES: [
{
CONF_HOST: "fake_host",
CONF_PASSWORD: "fake_pass",
CONF_USERNAME: "fake_user",
}
]
}
}
class FritzDeviceBinarySensorMock(Mock):
"""Mock of a AVM Fritz!Box binary sensor device."""
ain = "fake_ain"
alert_state = "fake_state"
fw_version = "1.2.3"
has_alarm = True
has_switch = False
has_temperature_sensor = False
has_thermostat = False
manufacturer = "fake_manufacturer"
name = "fake_name"
present = True
productname = "fake_productname"
class FritzDeviceClimateMock(Mock):
"""Mock of a AVM Fritz!Box climate device."""
actual_temperature = 18.0
ain = "fake_ain"
alert_state = "fake_state"
battery_level = 23
battery_low = True
comfort_temperature = 22.0
device_lock = "fake_locked_device"
eco_temperature = 16.0
fw_version = "1.2.3"
has_alarm = False
has_switch = False
has_temperature_sensor = False
has_thermostat = True
holiday_active = "fake_holiday"
lock = "fake_locked"
manufacturer = "fake_manufacturer"
name = "fake_name"
present = True
productname = "fake_productname"
summer_active = "fake_summer"
target_temperature = 19.5
window_open = "fake_window"
class FritzDeviceSensorMock(Mock):
"""Mock of a AVM Fritz!Box sensor device."""
ain = "fake_ain"
device_lock = "fake_locked_device"
fw_version = "1.2.3"
has_alarm = False
has_switch = False
has_temperature_sensor = True
has_thermostat = False
lock = "fake_locked"
manufacturer = "fake_manufacturer"
name = "fake_name"
present = True
productname = "fake_productname"
temperature = 1.23
class FritzDeviceSwitchMock(Mock):
"""Mock of a AVM Fritz!Box switch device."""
ain = "fake_ain"
device_lock = "fake_locked_device"
energy = 1234
fw_version = "1.2.3"
has_alarm = False
has_switch = True
has_temperature_sensor = True
has_thermostat = False
switch_state = "fake_state"
lock = "fake_locked"
manufacturer = "fake_manufacturer"
name = "fake_name"
power = 5678
present = True
productname = "fake_productname"
temperature = 135

View File

@ -0,0 +1,14 @@
"""Fixtures for the AVM Fritz!Box integration."""
from unittest.mock import Mock, patch
import pytest
@pytest.fixture(name="fritz")
def fritz_fixture() -> Mock:
"""Patch libraries."""
with patch("homeassistant.components.fritzbox.socket") as socket, patch(
"homeassistant.components.fritzbox.Fritzhome"
) as fritz, patch("homeassistant.components.fritzbox.config_flow.Fritzhome"):
socket.gethostbyname.return_value = "FAKE_IP_ADDRESS"
yield fritz

View File

@ -0,0 +1,94 @@
"""Tests for AVM Fritz!Box binary sensor component."""
from datetime import timedelta
from unittest import mock
from unittest.mock import Mock
from requests.exceptions import HTTPError
from homeassistant.components.binary_sensor import DOMAIN
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
STATE_OFF,
STATE_ON,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import MOCK_CONFIG, FritzDeviceBinarySensorMock
from tests.common import async_fire_time_changed
ENTITY_ID = f"{DOMAIN}.fake_name"
async def setup_fritzbox(hass: HomeAssistantType, config: dict):
"""Set up mock AVM Fritz!Box."""
assert await async_setup_component(hass, FB_DOMAIN, config)
await hass.async_block_till_done()
async def test_setup(hass: HomeAssistantType, fritz: Mock):
"""Test setup of platform."""
device = FritzDeviceBinarySensorMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
assert state.attributes[ATTR_DEVICE_CLASS] == "window"
async def test_is_off(hass: HomeAssistantType, fritz: Mock):
"""Test state of platform."""
device = FritzDeviceBinarySensorMock()
device.present = False
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == STATE_OFF
async def test_update(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceBinarySensorMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 1
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 2
assert fritz().login.call_count == 1
async def test_update_error(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceBinarySensorMock()
device.update.side_effect = [mock.DEFAULT, HTTPError("Boom")]
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 1
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 2
assert fritz().login.call_count == 2

View File

@ -1,151 +1,306 @@
"""The tests for the demo climate component.""" """Tests for AVM Fritz!Box climate component."""
import unittest from datetime import timedelta
from unittest.mock import Mock, patch from unittest.mock import Mock, call
import requests from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.climate import FritzboxThermostat from homeassistant.components.climate.const import (
from homeassistant.const import TEMP_CELSIUS ATTR_CURRENT_TEMPERATURE,
ATTR_HVAC_MODE,
ATTR_HVAC_MODES,
ATTR_MAX_TEMP,
ATTR_MIN_TEMP,
ATTR_PRESET_MODE,
ATTR_PRESET_MODES,
DOMAIN,
HVAC_MODE_HEAT,
HVAC_MODE_OFF,
PRESET_COMFORT,
PRESET_ECO,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
)
from homeassistant.components.fritzbox.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,
DOMAIN as FB_DOMAIN,
)
from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_TEMPERATURE,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import MOCK_CONFIG, FritzDeviceClimateMock
from tests.common import async_fire_time_changed
ENTITY_ID = f"{DOMAIN}.fake_name"
class TestFritzboxClimate(unittest.TestCase): async def setup_fritzbox(hass: HomeAssistantType, config: dict):
"""Test Fritz!Box heating thermostats.""" """Set up mock AVM Fritz!Box."""
assert await async_setup_component(hass, FB_DOMAIN, config) is True
await hass.async_block_till_done()
def setUp(self):
"""Create a mock device to test on."""
self.device = Mock()
self.device.name = "Test Thermostat"
self.device.actual_temperature = 18.0
self.device.target_temperature = 19.5
self.device.comfort_temperature = 22.0
self.device.eco_temperature = 16.0
self.device.present = True
self.device.device_lock = True
self.device.lock = False
self.device.battery_low = True
self.device.set_target_temperature = Mock()
self.device.update = Mock()
mock_fritz = Mock()
mock_fritz.login = Mock()
self.thermostat = FritzboxThermostat(self.device, mock_fritz)
def test_init(self): async def test_setup(hass: HomeAssistantType, fritz: Mock):
"""Test instance creation.""" """Test setup of platform."""
assert 18.0 == self.thermostat._current_temperature device = FritzDeviceClimateMock()
assert 19.5 == self.thermostat._target_temperature fritz().get_devices.return_value = [device]
assert 22.0 == self.thermostat._comfort_temperature
assert 16.0 == self.thermostat._eco_temperature
def test_supported_features(self): await setup_fritzbox(hass, MOCK_CONFIG)
"""Test supported features property.""" state = hass.states.get(ENTITY_ID)
assert self.thermostat.supported_features == 17
def test_available(self): assert state
"""Test available property.""" assert state.attributes[ATTR_BATTERY_LEVEL] == 23
assert self.thermostat.available assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18
self.thermostat._device.present = False assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
assert not self.thermostat.available assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT, HVAC_MODE_OFF]
assert state.attributes[ATTR_MAX_TEMP] == 28
assert state.attributes[ATTR_MIN_TEMP] == 8
assert state.attributes[ATTR_PRESET_MODE] is None
assert state.attributes[ATTR_PRESET_MODES] == [PRESET_ECO, PRESET_COMFORT]
assert state.attributes[ATTR_STATE_BATTERY_LOW] is True
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_HOLIDAY_MODE] == "fake_holiday"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_STATE_SUMMER_MODE] == "fake_summer"
assert state.attributes[ATTR_STATE_WINDOW_OPEN] == "fake_window"
assert state.attributes[ATTR_TEMPERATURE] == 19.5
assert state.state == HVAC_MODE_HEAT
def test_name(self):
"""Test name property."""
assert "Test Thermostat" == self.thermostat.name
def test_temperature_unit(self): async def test_target_temperature_on(hass: HomeAssistantType, fritz: Mock):
"""Test temperature_unit property.""" """Test turn device on."""
assert TEMP_CELSIUS == self.thermostat.temperature_unit device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
device.target_temperature = 127.0
def test_precision(self): await setup_fritzbox(hass, MOCK_CONFIG)
"""Test precision property.""" state = hass.states.get(ENTITY_ID)
assert 0.5 == self.thermostat.precision assert state
assert state.attributes[ATTR_TEMPERATURE] == 30
def test_current_temperature(self):
"""Test current_temperature property incl. special temperatures."""
assert 18 == self.thermostat.current_temperature
def test_target_temperature(self): async def test_target_temperature_off(hass: HomeAssistantType, fritz: Mock):
"""Test target_temperature property.""" """Test turn device on."""
assert 19.5 == self.thermostat.target_temperature device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
device.target_temperature = 126.5
self.thermostat._target_temperature = 126.5 await setup_fritzbox(hass, MOCK_CONFIG)
assert self.thermostat.target_temperature == 0.0 state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_TEMPERATURE] == 0
self.thermostat._target_temperature = 127.0
assert self.thermostat.target_temperature == 30.0
@patch.object(FritzboxThermostat, "set_hvac_mode") async def test_update(hass: HomeAssistantType, fritz: Mock):
def test_set_temperature_operation_mode(self, mock_set_op): """Test update with error."""
"""Test set_temperature by operation_mode.""" device = FritzDeviceClimateMock()
self.thermostat.set_temperature(hvac_mode="heat") fritz().get_devices.return_value = [device]
mock_set_op.assert_called_once_with("heat")
def test_set_temperature_temperature(self): await setup_fritzbox(hass, MOCK_CONFIG)
"""Test set_temperature by temperature.""" state = hass.states.get(ENTITY_ID)
self.thermostat.set_temperature(temperature=23.0)
self.thermostat._device.set_target_temperature.assert_called_once_with(23.0)
@patch.object(FritzboxThermostat, "set_hvac_mode") assert state
def test_set_temperature_none(self, mock_set_op): assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 18
"""Test set_temperature with no arguments.""" assert state.attributes[ATTR_MAX_TEMP] == 28
self.thermostat.set_temperature() assert state.attributes[ATTR_MIN_TEMP] == 8
mock_set_op.assert_not_called() assert state.attributes[ATTR_TEMPERATURE] == 19.5
self.thermostat._device.set_target_temperature.assert_not_called()
@patch.object(FritzboxThermostat, "set_hvac_mode") device.actual_temperature = 19
def test_set_temperature_operation_mode_precedence(self, mock_set_op): device.target_temperature = 20
"""Test set_temperature for precedence of operation_mode argument."""
self.thermostat.set_temperature(hvac_mode="heat", temperature=23.0)
mock_set_op.assert_called_once_with("heat")
self.thermostat._device.set_target_temperature.assert_not_called()
def test_hvac_mode(self): next_update = dt_util.utcnow() + timedelta(seconds=200)
"""Test operation mode property for different temperatures.""" async_fire_time_changed(hass, next_update)
self.thermostat._target_temperature = 127.0 await hass.async_block_till_done()
assert "heat" == self.thermostat.hvac_mode state = hass.states.get(ENTITY_ID)
self.thermostat._target_temperature = 126.5
assert "off" == self.thermostat.hvac_mode
self.thermostat._target_temperature = 22.0
assert "heat" == self.thermostat.hvac_mode
self.thermostat._target_temperature = 16.0
assert "heat" == self.thermostat.hvac_mode
self.thermostat._target_temperature = 12.5
assert "heat" == self.thermostat.hvac_mode
def test_operation_list(self): assert device.update.call_count == 1
"""Test operation_list property.""" assert state
assert ["heat", "off"] == self.thermostat.hvac_modes assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19
assert state.attributes[ATTR_TEMPERATURE] == 20
def test_min_max_temperature(self):
"""Test min_temp and max_temp properties."""
assert 8.0 == self.thermostat.min_temp
assert 28.0 == self.thermostat.max_temp
def test_device_state_attributes(self): async def test_update_error(hass: HomeAssistantType, fritz: Mock):
"""Test device_state property.""" """Test update with error."""
attr = self.thermostat.device_state_attributes device = FritzDeviceClimateMock()
assert attr["device_locked"] is True device.update.side_effect = HTTPError("Boom")
assert attr["locked"] is False fritz().get_devices.return_value = [device]
assert attr["battery_low"] is True
def test_update(self): await setup_fritzbox(hass, MOCK_CONFIG)
"""Test update function.""" assert device.update.call_count == 0
device = Mock() assert fritz().login.call_count == 1
device.update = Mock()
device.actual_temperature = 10.0
device.target_temperature = 11.0
device.comfort_temperature = 12.0
device.eco_temperature = 13.0
self.thermostat._device = device
self.thermostat.update() next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
device.update.assert_called_once_with() assert device.update.call_count == 1
assert 10.0 == self.thermostat._current_temperature assert fritz().login.call_count == 2
assert 11.0 == self.thermostat._target_temperature
assert 12.0 == self.thermostat._comfort_temperature
assert 13.0 == self.thermostat._eco_temperature
def test_update_http_error(self):
"""Test exception handling of update function.""" async def test_set_temperature_temperature(hass: HomeAssistantType, fritz: Mock):
self.device.update.side_effect = requests.exceptions.HTTPError """Test setting temperature by temperature."""
self.thermostat.update() device = FritzDeviceClimateMock()
self.thermostat._fritz.login.assert_called_once_with() fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 123},
True,
)
assert device.set_target_temperature.call_args_list == [call(123)]
async def test_set_temperature_mode_off(hass: HomeAssistantType, fritz: Mock):
"""Test setting temperature by mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_HVAC_MODE: HVAC_MODE_OFF,
ATTR_TEMPERATURE: 123,
},
True,
)
assert device.set_target_temperature.call_args_list == [call(0)]
async def test_set_temperature_mode_heat(hass: HomeAssistantType, fritz: Mock):
"""Test setting temperature by mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_TEMPERATURE,
{
ATTR_ENTITY_ID: ENTITY_ID,
ATTR_HVAC_MODE: HVAC_MODE_HEAT,
ATTR_TEMPERATURE: 123,
},
True,
)
assert device.set_target_temperature.call_args_list == [call(22)]
async def test_set_hvac_mode_off(hass: HomeAssistantType, fritz: Mock):
"""Test setting hvac mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_OFF},
True,
)
assert device.set_target_temperature.call_args_list == [call(0)]
async def test_set_hvac_mode_heat(hass: HomeAssistantType, fritz: Mock):
"""Test setting hvac mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_HEAT},
True,
)
assert device.set_target_temperature.call_args_list == [call(22)]
async def test_set_preset_mode_comfort(hass: HomeAssistantType, fritz: Mock):
"""Test setting preset mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_COMFORT},
True,
)
assert device.set_target_temperature.call_args_list == [call(22)]
async def test_set_preset_mode_eco(hass: HomeAssistantType, fritz: Mock):
"""Test setting preset mode."""
device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: PRESET_ECO},
True,
)
assert device.set_target_temperature.call_args_list == [call(16)]
async def test_preset_mode_update(hass: HomeAssistantType, fritz: Mock):
"""Test preset mode."""
device = FritzDeviceClimateMock()
device.comfort_temperature = 98
device.eco_temperature = 99
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
state = hass.states.get(ENTITY_ID)
assert state
assert state.attributes[ATTR_PRESET_MODE] is None
device.target_temperature = 98
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert device.update.call_count == 1
assert state
assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT
device.target_temperature = 99
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert device.update.call_count == 2
assert state
assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO

View File

@ -0,0 +1,179 @@
"""Tests for AVM Fritz!Box config flow."""
from unittest import mock
from unittest.mock import Mock, patch
from pyfritzhome import LoginError
import pytest
from homeassistant.components.fritzbox.const import DOMAIN
from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_FRIENDLY_NAME
from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.typing import HomeAssistantType
from . import MOCK_CONFIG
MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0]
MOCK_SSDP_DATA = {
ATTR_SSDP_LOCATION: "https://fake_host:12345/test",
ATTR_UPNP_FRIENDLY_NAME: "fake_name",
}
@pytest.fixture(name="fritz")
def fritz_fixture() -> Mock:
"""Patch libraries."""
with patch("homeassistant.components.fritzbox.config_flow.Fritzhome") as fritz:
yield fritz
async def test_user(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow by user."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}
)
assert result["type"] == "form"
assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], user_input=MOCK_USER_DATA
)
assert result["type"] == "create_entry"
assert result["title"] == "fake_host"
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
async def test_user_auth_failed(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow by user with authentication failure."""
fritz().login.side_effect = [LoginError("Boom"), mock.DEFAULT]
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "form"
assert result["step_id"] == "user"
assert result["errors"]["base"] == "auth_failed"
async def test_user_not_successful(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow by user but no connection found."""
fritz().login.side_effect = OSError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "abort"
assert result["reason"] == "not_found"
async def test_user_already_configured(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow by user when already configured."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "create_entry"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
async def test_import(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow by import."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "import"}, data=MOCK_USER_DATA
)
assert result["type"] == "create_entry"
assert result["title"] == "fake_host"
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
async def test_ssdp(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow from discovery."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result["type"] == "form"
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "fake_pass", CONF_USERNAME: "fake_user"},
)
assert result["type"] == "create_entry"
assert result["title"] == "fake_name"
assert result["data"][CONF_HOST] == "fake_host"
assert result["data"][CONF_PASSWORD] == "fake_pass"
assert result["data"][CONF_USERNAME] == "fake_user"
async def test_ssdp_auth_failed(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow from discovery with authentication failure."""
fritz().login.side_effect = LoginError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result["type"] == "form"
assert result["step_id"] == "confirm"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"},
)
assert result["type"] == "form"
assert result["step_id"] == "confirm"
assert result["errors"]["base"] == "auth_failed"
async def test_ssdp_not_successful(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow from discovery but no device found."""
fritz().login.side_effect = OSError("Boom")
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result["type"] == "form"
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_PASSWORD: "whatever", CONF_USERNAME: "whatever"},
)
assert result["type"] == "abort"
assert result["reason"] == "not_found"
async def test_ssdp_already_in_progress(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow from discovery twice."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result["type"] == "form"
assert result["step_id"] == "confirm"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result["type"] == "abort"
assert result["reason"] == "already_in_progress"
async def test_ssdp_already_configured(hass: HomeAssistantType, fritz: Mock):
"""Test starting a flow from discovery when already configured."""
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA
)
assert result["type"] == "create_entry"
result2 = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "ssdp"}, data=MOCK_SSDP_DATA
)
assert result2["type"] == "abort"
assert result2["reason"] == "already_configured"

View File

@ -0,0 +1,76 @@
"""Tests for the AVM Fritz!Box integration."""
from unittest.mock import Mock, call
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
from . import MOCK_CONFIG, FritzDeviceSwitchMock
from tests.common import MockConfigEntry
async def test_setup(hass: HomeAssistantType, fritz: Mock):
"""Test setup of integration."""
assert await async_setup_component(hass, FB_DOMAIN, MOCK_CONFIG)
await hass.async_block_till_done()
entries = hass.config_entries.async_entries()
assert entries
assert entries[0].data[CONF_HOST] == "fake_host"
assert entries[0].data[CONF_PASSWORD] == "fake_pass"
assert entries[0].data[CONF_USERNAME] == "fake_user"
assert fritz.call_count == 1
assert fritz.call_args_list == [
call(host="fake_host", password="fake_pass", user="fake_user")
]
async def test_setup_duplicate_config(hass: HomeAssistantType, fritz: Mock, caplog):
"""Test duplicate config of integration."""
DUPLICATE = {
FB_DOMAIN: {
CONF_DEVICES: [
MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
]
}
}
assert not await async_setup_component(hass, FB_DOMAIN, DUPLICATE)
await hass.async_block_till_done()
assert not hass.states.async_entity_ids()
assert not hass.states.async_all()
assert "duplicate host entries found" in caplog.text
async def test_unload(hass: HomeAssistantType, fritz: Mock):
"""Test unload of integration."""
fritz().get_devices.return_value = [FritzDeviceSwitchMock()]
entity_id = f"{SWITCH_DOMAIN}.fake_name"
entry = MockConfigEntry(
domain=FB_DOMAIN,
data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
unique_id=entity_id,
)
entry.add_to_hass(hass)
config_entries = hass.config_entries.async_entries(FB_DOMAIN)
assert len(config_entries) == 1
assert entry is config_entries[0]
assert await async_setup_component(hass, FB_DOMAIN, {}) is True
await hass.async_block_till_done()
assert entry.state == ENTRY_STATE_LOADED
state = hass.states.get(entity_id)
assert state
await hass.config_entries.async_unload(entry.entry_id)
assert fritz().logout.call_count == 1
assert entry.state == ENTRY_STATE_NOT_LOADED
state = hass.states.get(entity_id)
assert state is None

View File

@ -0,0 +1,83 @@
"""Tests for AVM Fritz!Box sensor component."""
from datetime import timedelta
from unittest.mock import Mock
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.sensor import DOMAIN
from homeassistant.const import (
ATTR_FRIENDLY_NAME,
ATTR_UNIT_OF_MEASUREMENT,
TEMP_CELSIUS,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import MOCK_CONFIG, FritzDeviceSensorMock
from tests.common import async_fire_time_changed
ENTITY_ID = f"{DOMAIN}.fake_name"
async def setup_fritzbox(hass: HomeAssistantType, config: dict):
"""Set up mock AVM Fritz!Box."""
assert await async_setup_component(hass, FB_DOMAIN, config)
await hass.async_block_till_done()
async def test_setup(hass: HomeAssistantType, fritz: Mock):
"""Test setup of platform."""
device = FritzDeviceSensorMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == "1.23"
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
async def test_update(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceSensorMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 1
assert fritz().login.call_count == 1
async def test_update_error(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceSensorMock()
device.update.side_effect = HTTPError("Boom")
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 1
assert fritz().login.call_count == 2

View File

@ -0,0 +1,121 @@
"""Tests for AVM Fritz!Box switch component."""
from datetime import timedelta
from unittest.mock import Mock
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import (
ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED,
ATTR_TEMPERATURE_UNIT,
ATTR_TOTAL_CONSUMPTION,
ATTR_TOTAL_CONSUMPTION_UNIT,
DOMAIN as FB_DOMAIN,
)
from homeassistant.components.switch import ATTR_CURRENT_POWER_W, DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
ATTR_TEMPERATURE,
ENERGY_KILO_WATT_HOUR,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_ON,
TEMP_CELSIUS,
)
from homeassistant.helpers.typing import HomeAssistantType
from homeassistant.setup import async_setup_component
import homeassistant.util.dt as dt_util
from . import MOCK_CONFIG, FritzDeviceSwitchMock
from tests.common import async_fire_time_changed
ENTITY_ID = f"{DOMAIN}.fake_name"
async def setup_fritzbox(hass: HomeAssistantType, config: dict):
"""Set up mock AVM Fritz!Box."""
assert await async_setup_component(hass, FB_DOMAIN, config)
await hass.async_block_till_done()
async def test_setup(hass: HomeAssistantType, fritz: Mock):
"""Test setup of platform."""
device = FritzDeviceSwitchMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
state = hass.states.get(ENTITY_ID)
assert state
assert state.state == STATE_ON
assert state.attributes[ATTR_CURRENT_POWER_W] == 5.678
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake_name"
assert state.attributes[ATTR_STATE_DEVICE_LOCKED] == "fake_locked_device"
assert state.attributes[ATTR_STATE_LOCKED] == "fake_locked"
assert state.attributes[ATTR_TEMPERATURE] == "135"
assert state.attributes[ATTR_TEMPERATURE_UNIT] == TEMP_CELSIUS
assert state.attributes[ATTR_TOTAL_CONSUMPTION] == "1.234"
assert state.attributes[ATTR_TOTAL_CONSUMPTION_UNIT] == ENERGY_KILO_WATT_HOUR
async def test_turn_on(hass: HomeAssistantType, fritz: Mock):
"""Test turn device on."""
device = FritzDeviceSwitchMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert device.set_switch_state_on.call_count == 1
async def test_turn_off(hass: HomeAssistantType, fritz: Mock):
"""Test turn device off."""
device = FritzDeviceSwitchMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert await hass.services.async_call(
DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID}, True
)
assert device.set_switch_state_off.call_count == 1
async def test_update(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceSwitchMock()
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 1
assert fritz().login.call_count == 1
async def test_update_error(hass: HomeAssistantType, fritz: Mock):
"""Test update with error."""
device = FritzDeviceSwitchMock()
device.update.side_effect = HTTPError("Boom")
fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0
assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
assert device.update.call_count == 1
assert fritz().login.call_count == 2