Implement DataUpdateCoordinator to fritzbox integration (#49611)

This commit is contained in:
Michael 2021-04-25 02:40:12 +02:00 committed by GitHub
parent f1d48ddfe3
commit a352516944
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 372 additions and 323 deletions

View file

@ -1,22 +1,43 @@
"""Support for AVM Fritz!Box smarthome devices.""" """Support for AVM Fritz!Box smarthome devices."""
from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta
import socket import socket
from pyfritzhome import Fritzhome, LoginError from pyfritzhome import Fritzhome, FritzhomeDevice, LoginError
import requests
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT,
CONF_DEVICES, CONF_DEVICES,
CONF_HOST, CONF_HOST,
CONF_PASSWORD, CONF_PASSWORD,
CONF_USERNAME, CONF_USERNAME,
EVENT_HOMEASSISTANT_STOP, EVENT_HOMEASSISTANT_STOP,
) )
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS from .const import (
CONF_CONNECTIONS,
CONF_COORDINATOR,
DEFAULT_HOST,
DEFAULT_USERNAME,
DOMAIN,
LOGGER,
PLATFORMS,
)
def ensure_unique_hosts(value): def ensure_unique_hosts(value):
@ -58,7 +79,7 @@ CONFIG_SCHEMA = vol.Schema(
) )
async def async_setup(hass, config): async def async_setup(hass: HomeAssistant, config: dict[str, str]) -> bool:
"""Set up the AVM Fritz!Box integration.""" """Set up the AVM Fritz!Box integration."""
if DOMAIN in config: if DOMAIN in config:
for entry_config in config[DOMAIN][CONF_DEVICES]: for entry_config in config[DOMAIN][CONF_DEVICES]:
@ -71,7 +92,7 @@ async def async_setup(hass, config):
return True return True
async def async_setup_entry(hass, entry): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up the AVM Fritz!Box platforms.""" """Set up the AVM Fritz!Box platforms."""
fritz = Fritzhome( fritz = Fritzhome(
host=entry.data[CONF_HOST], host=entry.data[CONF_HOST],
@ -84,8 +105,44 @@ async def async_setup_entry(hass, entry):
except LoginError as err: except LoginError as err:
raise ConfigEntryAuthFailed from err raise ConfigEntryAuthFailed from err
hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz hass.data[DOMAIN][entry.entry_id] = {
CONF_CONNECTIONS: fritz,
}
def _update_fritz_devices() -> dict[str, FritzhomeDevice]:
"""Update all fritzbox device data."""
try:
devices = fritz.get_devices()
except requests.exceptions.HTTPError:
# If the device rebooted, login again
try:
fritz.login()
except requests.exceptions.HTTPError as ex:
raise ConfigEntryAuthFailed from ex
devices = fritz.get_devices()
data = {}
for device in devices:
device.update()
data[device.ain] = device
return data
async def async_update_coordinator():
"""Fetch all device data."""
return await hass.async_add_executor_job(_update_fritz_devices)
hass.data[DOMAIN][entry.entry_id][
CONF_COORDINATOR
] = coordinator = DataUpdateCoordinator(
hass,
LOGGER,
name=f"{entry.entry_id}",
update_method=async_update_coordinator,
update_interval=timedelta(seconds=30),
)
await coordinator.async_config_entry_first_refresh()
for platform in PLATFORMS: for platform in PLATFORMS:
hass.async_create_task( hass.async_create_task(
@ -103,9 +160,9 @@ async def async_setup_entry(hass, entry):
return True return True
async def async_unload_entry(hass, entry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unloading the AVM Fritz!Box platforms.""" """Unloading the AVM Fritz!Box platforms."""
fritz = hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] fritz = hass.data[DOMAIN][entry.entry_id][CONF_CONNECTIONS]
await hass.async_add_executor_job(fritz.logout) await hass.async_add_executor_job(fritz.logout)
unload_ok = all( unload_ok = all(
@ -117,6 +174,61 @@ async def async_unload_entry(hass, entry):
) )
) )
if unload_ok: if unload_ok:
hass.data[DOMAIN][CONF_CONNECTIONS].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return unload_ok
class FritzBoxEntity(CoordinatorEntity):
"""Basis FritzBox entity."""
def __init__(
self,
entity_info: dict[str, str],
coordinator: DataUpdateCoordinator,
ain: str,
):
"""Initialize the FritzBox entity."""
super().__init__(coordinator)
self.ain = ain
self._name = entity_info[ATTR_NAME]
self._unique_id = entity_info[ATTR_ENTITY_ID]
self._unit_of_measurement = entity_info[ATTR_UNIT_OF_MEASUREMENT]
self._device_class = entity_info[ATTR_DEVICE_CLASS]
@property
def device(self) -> FritzhomeDevice:
"""Return device object from coordinator."""
return self.coordinator.data[self.ain]
@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self.device.name,
"identifiers": {(DOMAIN, self.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._unique_id
@property
def name(self):
"""Return the name of the device."""
return self._name
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return self._unit_of_measurement
@property
def device_class(self):
"""Return the device class."""
return self._device_class

View file

@ -1,74 +1,56 @@
"""Support for Fritzbox binary sensors.""" """Support for Fritzbox binary sensors."""
import requests from typing import Callable
from homeassistant.components.binary_sensor import ( from homeassistant.components.binary_sensor import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
BinarySensorEntity, BinarySensorEntity,
) )
from homeassistant.const import CONF_DEVICES from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT,
)
from homeassistant.core import HomeAssistant
from .const import CONF_CONNECTIONS, DOMAIN as FRITZBOX_DOMAIN, LOGGER from . import FritzBoxEntity
from .const import CONF_COORDINATOR, DOMAIN as FRITZBOX_DOMAIN
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up the Fritzbox binary sensor from config_entry.""" hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up the Fritzbox binary sensor from ConfigEntry."""
entities = [] entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for device in await hass.async_add_executor_job(fritz.get_devices): for ain, device in coordinator.data.items():
if device.has_alarm and device.ain not in devices: if not device.has_alarm:
entities.append(FritzboxBinarySensor(device, fritz)) continue
devices.add(device.ain)
async_add_entities(entities, True) entities.append(
FritzboxBinarySensor(
{
ATTR_NAME: f"{device.name}",
ATTR_ENTITY_ID: f"{device.ain}",
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: DEVICE_CLASS_WINDOW,
},
coordinator,
ain,
)
)
async_add_entities(entities)
class FritzboxBinarySensor(BinarySensorEntity): class FritzboxBinarySensor(FritzBoxEntity, BinarySensorEntity):
"""Representation of a binary Fritzbox device.""" """Representation of a binary Fritzbox device."""
def __init__(self, device, fritz):
"""Initialize the Fritzbox binary sensor."""
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."""
return self._device.name
@property
def device_class(self):
"""Return the class of this sensor."""
return DEVICE_CLASS_WINDOW
@property @property
def is_on(self): def is_on(self):
"""Return true if sensor is on.""" """Return true if sensor is on."""
if not self._device.present: if not self.device.present:
return False return False
return self._device.alert_state return self.device.alert_state
def update(self):
"""Get latest data from the Fritzbox."""
try:
self._device.update()
except requests.exceptions.HTTPError as ex:
LOGGER.warning("Connection error: %s", ex)
self._fritz.login()

View file

@ -1,5 +1,5 @@
"""Support for AVM Fritz!Box smarthome thermostate devices.""" """Support for AVM Fritz!Box smarthome thermostate devices."""
import requests from typing import Callable
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -11,14 +11,20 @@ from homeassistant.components.climate.const import (
SUPPORT_PRESET_MODE, SUPPORT_PRESET_MODE,
SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE,
) )
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_DEVICES, ATTR_UNIT_OF_MEASUREMENT,
PRECISION_HALVES, PRECISION_HALVES,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant
from . import FritzBoxEntity
from .const import ( from .const import (
ATTR_STATE_BATTERY_LOW, ATTR_STATE_BATTERY_LOW,
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_DEVICE_LOCKED,
@ -26,9 +32,8 @@ from .const import (
ATTR_STATE_LOCKED, ATTR_STATE_LOCKED,
ATTR_STATE_SUMMER_MODE, ATTR_STATE_SUMMER_MODE,
ATTR_STATE_WINDOW_OPEN, ATTR_STATE_WINDOW_OPEN,
CONF_CONNECTIONS, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
) )
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE
@ -47,48 +52,36 @@ ON_REPORT_SET_TEMPERATURE = 30.0
OFF_REPORT_SET_TEMPERATURE = 0.0 OFF_REPORT_SET_TEMPERATURE = 0.0
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up the Fritzbox smarthome thermostat from config_entry.""" hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up the Fritzbox smarthome thermostat from ConfigEntry."""
entities = [] entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for device in await hass.async_add_executor_job(fritz.get_devices): for ain, device in coordinator.data.items():
if device.has_thermostat and device.ain not in devices: if not device.has_thermostat:
entities.append(FritzboxThermostat(device, fritz)) continue
devices.add(device.ain)
entities.append(
FritzboxThermostat(
{
ATTR_NAME: f"{device.name}",
ATTR_ENTITY_ID: f"{device.ain}",
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: None,
},
coordinator,
ain,
)
)
async_add_entities(entities) async_add_entities(entities)
class FritzboxThermostat(ClimateEntity): class FritzboxThermostat(FritzBoxEntity, ClimateEntity):
"""The thermostat class for Fritzbox smarthome thermostates.""" """The thermostat class for Fritzbox smarthome thermostates."""
def __init__(self, device, fritz):
"""Initialize the thermostat."""
self._device = device
self._fritz = fritz
self._current_temperature = self._device.actual_temperature
self._target_temperature = self._device.target_temperature
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 @property
def supported_features(self): def supported_features(self):
"""Return the list of supported features.""" """Return the list of supported features."""
@ -97,12 +90,7 @@ class FritzboxThermostat(ClimateEntity):
@property @property
def available(self): def available(self):
"""Return if thermostat is available.""" """Return if thermostat is available."""
return self._device.present return self.device.present
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property @property
def temperature_unit(self): def temperature_unit(self):
@ -117,32 +105,35 @@ class FritzboxThermostat(ClimateEntity):
@property @property
def current_temperature(self): def current_temperature(self):
"""Return the current temperature.""" """Return the current temperature."""
return self._current_temperature return self.device.actual_temperature
@property @property
def target_temperature(self): def target_temperature(self):
"""Return the temperature we try to reach.""" """Return the temperature we try to reach."""
if self._target_temperature == ON_API_TEMPERATURE: if self.device.target_temperature == ON_API_TEMPERATURE:
return ON_REPORT_SET_TEMPERATURE return ON_REPORT_SET_TEMPERATURE
if self._target_temperature == OFF_API_TEMPERATURE: if self.device.target_temperature == OFF_API_TEMPERATURE:
return OFF_REPORT_SET_TEMPERATURE return OFF_REPORT_SET_TEMPERATURE
return self._target_temperature return self.device.target_temperature
def set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
if ATTR_HVAC_MODE in kwargs: if ATTR_HVAC_MODE in kwargs:
hvac_mode = kwargs.get(ATTR_HVAC_MODE) hvac_mode = kwargs.get(ATTR_HVAC_MODE)
self.set_hvac_mode(hvac_mode) await self.async_set_hvac_mode(hvac_mode)
elif ATTR_TEMPERATURE in kwargs: elif ATTR_TEMPERATURE in kwargs:
temperature = kwargs.get(ATTR_TEMPERATURE) temperature = kwargs.get(ATTR_TEMPERATURE)
self._device.set_target_temperature(temperature) await self.hass.async_add_executor_job(
self.device.set_target_temperature, temperature
)
await self.coordinator.async_refresh()
@property @property
def hvac_mode(self): def hvac_mode(self):
"""Return the current operation mode.""" """Return the current operation mode."""
if ( if (
self._target_temperature == OFF_REPORT_SET_TEMPERATURE self.device.target_temperature == OFF_REPORT_SET_TEMPERATURE
or self._target_temperature == OFF_API_TEMPERATURE or self.device.target_temperature == OFF_API_TEMPERATURE
): ):
return HVAC_MODE_OFF return HVAC_MODE_OFF
@ -153,19 +144,21 @@ class FritzboxThermostat(ClimateEntity):
"""Return the list of available operation modes.""" """Return the list of available operation modes."""
return OPERATION_LIST return OPERATION_LIST
def set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set new operation mode.""" """Set new operation mode."""
if hvac_mode == HVAC_MODE_OFF: if hvac_mode == HVAC_MODE_OFF:
self.set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE) await self.async_set_temperature(temperature=OFF_REPORT_SET_TEMPERATURE)
else: else:
self.set_temperature(temperature=self._comfort_temperature) await self.async_set_temperature(
temperature=self.device.comfort_temperature
)
@property @property
def preset_mode(self): def preset_mode(self):
"""Return current preset mode.""" """Return current preset mode."""
if self._target_temperature == self._comfort_temperature: if self.device.target_temperature == self.device.comfort_temperature:
return PRESET_COMFORT return PRESET_COMFORT
if self._target_temperature == self._eco_temperature: if self.device.target_temperature == self.device.eco_temperature:
return PRESET_ECO return PRESET_ECO
@property @property
@ -173,12 +166,14 @@ class FritzboxThermostat(ClimateEntity):
"""Return supported preset modes.""" """Return supported preset modes."""
return [PRESET_ECO, PRESET_COMFORT] return [PRESET_ECO, PRESET_COMFORT]
def set_preset_mode(self, preset_mode): async def async_set_preset_mode(self, preset_mode):
"""Set preset mode.""" """Set preset mode."""
if preset_mode == PRESET_COMFORT: if preset_mode == PRESET_COMFORT:
self.set_temperature(temperature=self._comfort_temperature) await self.async_set_temperature(
temperature=self.device.comfort_temperature
)
elif preset_mode == PRESET_ECO: elif preset_mode == PRESET_ECO:
self.set_temperature(temperature=self._eco_temperature) await self.async_set_temperature(temperature=self.device.eco_temperature)
@property @property
def min_temp(self): def min_temp(self):
@ -194,31 +189,19 @@ class FritzboxThermostat(ClimateEntity):
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the device specific state attributes.""" """Return the device specific state attributes."""
attrs = { attrs = {
ATTR_STATE_BATTERY_LOW: self._device.battery_low, ATTR_STATE_BATTERY_LOW: self.device.battery_low,
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self._device.lock, ATTR_STATE_LOCKED: self.device.lock,
} }
# the following attributes are available since fritzos 7 # the following attributes are available since fritzos 7
if self._device.battery_level is not None: if self.device.battery_level is not None:
attrs[ATTR_BATTERY_LEVEL] = self._device.battery_level attrs[ATTR_BATTERY_LEVEL] = self.device.battery_level
if self._device.holiday_active is not None: if self.device.holiday_active is not None:
attrs[ATTR_STATE_HOLIDAY_MODE] = self._device.holiday_active attrs[ATTR_STATE_HOLIDAY_MODE] = self.device.holiday_active
if self._device.summer_active is not None: if self.device.summer_active is not None:
attrs[ATTR_STATE_SUMMER_MODE] = self._device.summer_active attrs[ATTR_STATE_SUMMER_MODE] = self.device.summer_active
if ATTR_STATE_WINDOW_OPEN is not None: if ATTR_STATE_WINDOW_OPEN is not None:
attrs[ATTR_STATE_WINDOW_OPEN] = self._device.window_open attrs[ATTR_STATE_WINDOW_OPEN] = self.device.window_open
return attrs return attrs
def update(self):
"""Update the data from the thermostat."""
try:
self._device.update()
self._current_temperature = self._device.actual_temperature
self._target_temperature = self._device.target_temperature
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)
self._fritz.login()

View file

@ -14,12 +14,13 @@ ATTR_TOTAL_CONSUMPTION = "total_consumption"
ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit" ATTR_TOTAL_CONSUMPTION_UNIT = "total_consumption_unit"
CONF_CONNECTIONS = "connections" CONF_CONNECTIONS = "connections"
CONF_COORDINATOR = "coordinator"
DEFAULT_HOST = "fritz.box" DEFAULT_HOST = "fritz.box"
DEFAULT_USERNAME = "admin" DEFAULT_USERNAME = "admin"
DOMAIN = "fritzbox" DOMAIN = "fritzbox"
LOGGER = logging.getLogger(__package__) LOGGER: logging.Logger = logging.getLogger(__package__)
PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"] PLATFORMS = ["binary_sensor", "climate", "switch", "sensor"]

View file

@ -1,143 +1,93 @@
"""Support for AVM Fritz!Box smarthome temperature sensor only devices.""" """Support for AVM Fritz!Box smarthome temperature sensor only devices."""
import requests from typing import Callable
from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
CONF_DEVICES, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
PERCENTAGE, PERCENTAGE,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant
from . import FritzBoxEntity
from .const import ( from .const import (
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED, ATTR_STATE_LOCKED,
CONF_CONNECTIONS, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
) )
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up the Fritzbox smarthome sensor from config_entry.""" hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up the Fritzbox smarthome sensor from ConfigEntry."""
entities = [] entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for device in await hass.async_add_executor_job(fritz.get_devices): for ain, device in coordinator.data.items():
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
): ):
entities.append(FritzBoxTempSensor(device, fritz)) entities.append(
devices.add(device.ain) FritzBoxTempSensor(
{
ATTR_NAME: f"{device.name}",
ATTR_ENTITY_ID: f"{device.ain}",
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
ATTR_DEVICE_CLASS: None,
},
coordinator,
ain,
)
)
if device.battery_level is not None: if device.battery_level is not None:
entities.append(FritzBoxBatterySensor(device, fritz)) entities.append(
devices.add(f"{device.ain}_battery") FritzBoxBatterySensor(
{
ATTR_NAME: f"{device.name} Battery",
ATTR_ENTITY_ID: f"{device.ain}_battery",
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
},
coordinator,
ain,
)
)
async_add_entities(entities) async_add_entities(entities)
class FritzBoxBatterySensor(SensorEntity): class FritzBoxBatterySensor(FritzBoxEntity, SensorEntity):
"""The entity class for Fritzbox battery sensors.""" """The entity class for Fritzbox sensors."""
def __init__(self, device, fritz):
"""Initialize the sensor."""
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 f"{self._device.ain}_battery"
@property
def name(self):
"""Return the name of the device."""
return f"{self._device.name} Battery"
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._device.battery_level return self.device.battery_level
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return PERCENTAGE
@property
def device_class(self):
"""Return the device class."""
return DEVICE_CLASS_BATTERY
class FritzBoxTempSensor(SensorEntity): class FritzBoxTempSensor(FritzBoxEntity, SensorEntity):
"""The entity class for Fritzbox temperature sensors.""" """The entity class for Fritzbox temperature sensors."""
def __init__(self, device, fritz):
"""Initialize the switch."""
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 device."""
return self._device.name
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._device.temperature return self.device.temperature
@property
def unit_of_measurement(self):
"""Return the unit of measurement."""
return TEMP_CELSIUS
def update(self):
"""Get latest data and states from the device."""
try:
self._device.update()
except requests.exceptions.HTTPError as ex:
LOGGER.warning("Fritzhome connection error: %s", ex)
self._fritz.login()
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attrs = { attrs = {
ATTR_STATE_DEVICE_LOCKED: self._device.device_lock, ATTR_STATE_DEVICE_LOCKED: self.device.device_lock,
ATTR_STATE_LOCKED: self._device.lock, ATTR_STATE_LOCKED: self.device.lock,
} }
return attrs return attrs

View file

@ -1,113 +1,99 @@
"""Support for AVM Fritz!Box smarthome switch devices.""" """Support for AVM Fritz!Box smarthome switch devices."""
import requests from typing import Callable
from homeassistant.components.switch import SwitchEntity from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_NAME,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONF_DEVICES, ATTR_UNIT_OF_MEASUREMENT,
ENERGY_KILO_WATT_HOUR, ENERGY_KILO_WATT_HOUR,
TEMP_CELSIUS, TEMP_CELSIUS,
) )
from homeassistant.core import HomeAssistant
from . import FritzBoxEntity
from .const import ( from .const import (
ATTR_STATE_DEVICE_LOCKED, ATTR_STATE_DEVICE_LOCKED,
ATTR_STATE_LOCKED, ATTR_STATE_LOCKED,
ATTR_TEMPERATURE_UNIT, ATTR_TEMPERATURE_UNIT,
ATTR_TOTAL_CONSUMPTION, ATTR_TOTAL_CONSUMPTION,
ATTR_TOTAL_CONSUMPTION_UNIT, ATTR_TOTAL_CONSUMPTION_UNIT,
CONF_CONNECTIONS, CONF_COORDINATOR,
DOMAIN as FRITZBOX_DOMAIN, DOMAIN as FRITZBOX_DOMAIN,
LOGGER,
) )
ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR ATTR_TOTAL_CONSUMPTION_UNIT_VALUE = ENERGY_KILO_WATT_HOUR
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(
"""Set up the Fritzbox smarthome switch from config_entry.""" hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable
) -> None:
"""Set up the Fritzbox smarthome switch from ConfigEntry."""
entities = [] entities = []
devices = hass.data[FRITZBOX_DOMAIN][CONF_DEVICES] coordinator = hass.data[FRITZBOX_DOMAIN][entry.entry_id][CONF_COORDINATOR]
fritz = hass.data[FRITZBOX_DOMAIN][CONF_CONNECTIONS][config_entry.entry_id]
for device in await hass.async_add_executor_job(fritz.get_devices): for ain, device in coordinator.data.items():
if device.has_switch and device.ain not in devices: if not device.has_switch:
entities.append(FritzboxSwitch(device, fritz)) continue
devices.add(device.ain)
entities.append(
FritzboxSwitch(
{
ATTR_NAME: f"{device.name}",
ATTR_ENTITY_ID: f"{device.ain}",
ATTR_UNIT_OF_MEASUREMENT: None,
ATTR_DEVICE_CLASS: None,
},
coordinator,
ain,
)
)
async_add_entities(entities) async_add_entities(entities)
class FritzboxSwitch(SwitchEntity): class FritzboxSwitch(FritzBoxEntity, SwitchEntity):
"""The switch class for Fritzbox switches.""" """The switch class for Fritzbox switches."""
def __init__(self, device, fritz):
"""Initialize the switch."""
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 @property
def available(self): def available(self):
"""Return if switch is available.""" """Return if switch is available."""
return self._device.present return self.device.present
@property
def name(self):
"""Return the name of the device."""
return self._device.name
@property @property
def is_on(self): def is_on(self):
"""Return true if the switch is on.""" """Return true if the switch is on."""
return self._device.switch_state return self.device.switch_state
def turn_on(self, **kwargs): async def async_turn_on(self, **kwargs):
"""Turn the switch on.""" """Turn the switch on."""
self._device.set_switch_state_on() await self.hass.async_add_executor_job(self.device.set_switch_state_on)
await self.coordinator.async_refresh()
def turn_off(self, **kwargs): async def async_turn_off(self, **kwargs):
"""Turn the switch off.""" """Turn the switch off."""
self._device.set_switch_state_off() await self.hass.async_add_executor_job(self.device.set_switch_state_off)
await self.coordinator.async_refresh()
def update(self):
"""Get latest data and states from the device."""
try:
self._device.update()
except requests.exceptions.HTTPError as ex:
LOGGER.warning("Fritzhome connection error: %s", ex)
self._fritz.login()
@property @property
def extra_state_attributes(self): def extra_state_attributes(self):
"""Return the state attributes of the device.""" """Return the state attributes of the device."""
attrs = {} attrs = {}
attrs[ATTR_STATE_DEVICE_LOCKED] = self._device.device_lock attrs[ATTR_STATE_DEVICE_LOCKED] = self.device.device_lock
attrs[ATTR_STATE_LOCKED] = self._device.lock attrs[ATTR_STATE_LOCKED] = self.device.lock
if self._device.has_powermeter: if self.device.has_powermeter:
attrs[ attrs[
ATTR_TOTAL_CONSUMPTION ATTR_TOTAL_CONSUMPTION
] = f"{((self._device.energy or 0.0) / 1000):.3f}" ] = f"{((self.device.energy or 0.0) / 1000):.3f}"
attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE attrs[ATTR_TOTAL_CONSUMPTION_UNIT] = ATTR_TOTAL_CONSUMPTION_UNIT_VALUE
if self._device.has_temperature_sensor: if self.device.has_temperature_sensor:
attrs[ATTR_TEMPERATURE] = str( attrs[ATTR_TEMPERATURE] = str(
self.hass.config.units.temperature( self.hass.config.units.temperature(
self._device.temperature, TEMP_CELSIUS self.device.temperature, TEMP_CELSIUS
) )
) )
attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit attrs[ATTR_TEMPERATURE_UNIT] = self.hass.config.units.temperature_unit
@ -116,4 +102,4 @@ class FritzboxSwitch(SwitchEntity):
@property @property
def current_power_w(self): def current_power_w(self):
"""Return the current power usage in W.""" """Return the current power usage in W."""
return self._device.power / 1000 return self.device.power / 1000

View file

@ -58,7 +58,7 @@ async def test_is_off(hass: HomeAssistant, fritz: Mock):
async def test_update(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock):
"""Test update with error.""" """Test update without error."""
device = FritzDeviceBinarySensorMock() device = FritzDeviceBinarySensorMock()
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
@ -91,4 +91,4 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock):
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 2 assert device.update.call_count == 2
assert fritz().login.call_count == 2 assert fritz().login.call_count == 1

View file

@ -105,7 +105,7 @@ async def test_target_temperature_off(hass: HomeAssistant, fritz: Mock):
async def test_update(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock):
"""Test update with error.""" """Test update without error."""
device = FritzDeviceClimateMock() device = FritzDeviceClimateMock()
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
@ -126,7 +126,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock):
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert device.update.call_count == 1 assert device.update.call_count == 2
assert state assert state
assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19 assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 19
assert state.attributes[ATTR_TEMPERATURE] == 20 assert state.attributes[ATTR_TEMPERATURE] == 20
@ -139,14 +139,14 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock):
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG) await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0 assert device.update.call_count == 1
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200) next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 1 assert device.update.call_count == 2
assert fritz().login.call_count == 2 assert fritz().login.call_count == 2
@ -290,7 +290,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock):
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert device.update.call_count == 1 assert device.update.call_count == 2
assert state assert state
assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT
@ -301,6 +301,6 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock):
await hass.async_block_till_done() await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert device.update.call_count == 2 assert device.update.call_count == 3
assert state assert state
assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO

View file

@ -2,6 +2,7 @@
from unittest.mock import Mock, call, patch from unittest.mock import Mock, call, patch
from pyfritzhome import LoginError from pyfritzhome import LoginError
from requests.exceptions import HTTPError
from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
@ -57,6 +58,39 @@ async def test_setup_duplicate_config(hass: HomeAssistant, fritz: Mock, caplog):
assert "duplicate host entries found" in caplog.text assert "duplicate host entries found" in caplog.text
async def test_coordinator_update_after_reboot(hass: HomeAssistant, fritz: Mock):
"""Test coordinator after reboot."""
entry = MockConfigEntry(
domain=FB_DOMAIN,
data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
unique_id="any",
)
entry.add_to_hass(hass)
fritz().get_devices.side_effect = [HTTPError(), ""]
assert await hass.config_entries.async_setup(entry.entry_id)
assert fritz().get_devices.call_count == 2
assert fritz().login.call_count == 2
async def test_coordinator_update_after_password_change(
hass: HomeAssistant, fritz: Mock
):
"""Test coordinator after password change."""
entry = MockConfigEntry(
domain=FB_DOMAIN,
data=MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0],
unique_id="any",
)
entry.add_to_hass(hass)
fritz().get_devices.side_effect = HTTPError()
fritz().login.side_effect = ["", HTTPError()]
assert not await hass.config_entries.async_setup(entry.entry_id)
assert fritz().get_devices.call_count == 1
assert fritz().login.call_count == 2
async def test_unload_remove(hass: HomeAssistant, fritz: Mock): async def test_unload_remove(hass: HomeAssistant, fritz: Mock):
"""Test unload and remove of integration.""" """Test unload and remove of integration."""
fritz().get_devices.return_value = [FritzDeviceSwitchMock()] fritz().get_devices.return_value = [FritzDeviceSwitchMock()]
@ -107,9 +141,10 @@ async def test_raise_config_entry_not_ready_when_offline(hass):
with patch( with patch(
"homeassistant.components.fritzbox.Fritzhome.login", "homeassistant.components.fritzbox.Fritzhome.login",
side_effect=LoginError("user"), side_effect=LoginError("user"),
): ) as mock_login:
await hass.config_entries.async_setup(entry.entry_id) await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
mock_login.assert_called_once()
entries = hass.config_entries.async_entries() entries = hass.config_entries.async_entries()
config_entry = entries[0] config_entry = entries[0]

View file

@ -57,19 +57,19 @@ async def test_setup(hass: HomeAssistant, fritz: Mock):
async def test_update(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock):
"""Test update with error.""" """Test update without error."""
device = FritzDeviceSensorMock() device = FritzDeviceSensorMock()
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG) await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0 assert device.update.call_count == 1
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200) next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 1 assert device.update.call_count == 2
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
@ -80,12 +80,12 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock):
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG) await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0 assert device.update.call_count == 1
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200) next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 1 assert device.update.call_count == 2
assert fritz().login.call_count == 2 assert fritz().login.call_count == 2

View file

@ -87,19 +87,19 @@ async def test_turn_off(hass: HomeAssistant, fritz: Mock):
async def test_update(hass: HomeAssistant, fritz: Mock): async def test_update(hass: HomeAssistant, fritz: Mock):
"""Test update with error.""" """Test update without error."""
device = FritzDeviceSwitchMock() device = FritzDeviceSwitchMock()
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG) await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0 assert device.update.call_count == 1
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200) next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 1 assert device.update.call_count == 2
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
@ -110,12 +110,12 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock):
fritz().get_devices.return_value = [device] fritz().get_devices.return_value = [device]
await setup_fritzbox(hass, MOCK_CONFIG) await setup_fritzbox(hass, MOCK_CONFIG)
assert device.update.call_count == 0 assert device.update.call_count == 1
assert fritz().login.call_count == 1 assert fritz().login.call_count == 1
next_update = dt_util.utcnow() + timedelta(seconds=200) next_update = dt_util.utcnow() + timedelta(seconds=200)
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
await hass.async_block_till_done() await hass.async_block_till_done()
assert device.update.call_count == 1 assert device.update.call_count == 2
assert fritz().login.call_count == 2 assert fritz().login.call_count == 2