mirror of
https://github.com/home-assistant/core.git
synced 2025-07-14 16:57:10 +00:00
Turn AVM FRITZ!Box Tools call deflection switches into coordinator entities (#91913)
Co-authored-by: Franck Nijhof <frenck@frenck.nl>
This commit is contained in:
parent
d8bc37c695
commit
ac4d9216d6
@ -80,7 +80,10 @@ class FritzBoxBinarySensor(FritzBoxBaseCoordinatorEntity, BinarySensorEntity):
|
|||||||
def is_on(self) -> bool | None:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
if isinstance(
|
if isinstance(
|
||||||
state := self.coordinator.data.get(self.entity_description.key), bool
|
state := self.coordinator.data["entity_states"].get(
|
||||||
|
self.entity_description.key
|
||||||
|
),
|
||||||
|
bool,
|
||||||
):
|
):
|
||||||
return state
|
return state
|
||||||
return None
|
return None
|
||||||
|
@ -19,6 +19,7 @@ from fritzconnection.core.exceptions import (
|
|||||||
from fritzconnection.lib.fritzhosts import FritzHosts
|
from fritzconnection.lib.fritzhosts import FritzHosts
|
||||||
from fritzconnection.lib.fritzstatus import FritzStatus
|
from fritzconnection.lib.fritzstatus import FritzStatus
|
||||||
from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN
|
from fritzconnection.lib.fritzwlan import DEFAULT_PASSWORD_LENGTH, FritzGuestWLAN
|
||||||
|
import xmltodict
|
||||||
|
|
||||||
from homeassistant.components.device_tracker import (
|
from homeassistant.components.device_tracker import (
|
||||||
CONF_CONSIDER_HOME,
|
CONF_CONSIDER_HOME,
|
||||||
@ -137,8 +138,15 @@ class HostInfo(TypedDict):
|
|||||||
status: bool
|
status: bool
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateCoordinatorDataType(TypedDict):
|
||||||
|
"""Update coordinator data type."""
|
||||||
|
|
||||||
|
call_deflections: dict[int, dict]
|
||||||
|
entity_states: dict[str, StateType | bool]
|
||||||
|
|
||||||
|
|
||||||
class FritzBoxTools(
|
class FritzBoxTools(
|
||||||
update_coordinator.DataUpdateCoordinator[dict[str, bool | StateType]]
|
update_coordinator.DataUpdateCoordinator[UpdateCoordinatorDataType]
|
||||||
):
|
):
|
||||||
"""FritzBoxTools class."""
|
"""FritzBoxTools class."""
|
||||||
|
|
||||||
@ -173,6 +181,7 @@ class FritzBoxTools(
|
|||||||
self.password = password
|
self.password = password
|
||||||
self.port = port
|
self.port = port
|
||||||
self.username = username
|
self.username = username
|
||||||
|
self.has_call_deflections: bool = False
|
||||||
self._model: str | None = None
|
self._model: str | None = None
|
||||||
self._current_firmware: str | None = None
|
self._current_firmware: str | None = None
|
||||||
self._latest_firmware: str | None = None
|
self._latest_firmware: str | None = None
|
||||||
@ -243,6 +252,8 @@ class FritzBoxTools(
|
|||||||
)
|
)
|
||||||
self.device_is_router = self.fritz_status.has_wan_enabled
|
self.device_is_router = self.fritz_status.has_wan_enabled
|
||||||
|
|
||||||
|
self.has_call_deflections = "X_AVM-DE_OnTel1" in self.connection.services
|
||||||
|
|
||||||
def register_entity_updates(
|
def register_entity_updates(
|
||||||
self, key: str, update_fn: Callable[[FritzStatus, StateType], Any]
|
self, key: str, update_fn: Callable[[FritzStatus, StateType], Any]
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
@ -259,20 +270,30 @@ class FritzBoxTools(
|
|||||||
self._entity_update_functions[key] = update_fn
|
self._entity_update_functions[key] = update_fn
|
||||||
return unregister_entity_updates
|
return unregister_entity_updates
|
||||||
|
|
||||||
async def _async_update_data(self) -> dict[str, bool | StateType]:
|
async def _async_update_data(self) -> UpdateCoordinatorDataType:
|
||||||
"""Update FritzboxTools data."""
|
"""Update FritzboxTools data."""
|
||||||
enity_data: dict[str, bool | StateType] = {}
|
entity_data: UpdateCoordinatorDataType = {
|
||||||
|
"call_deflections": {},
|
||||||
|
"entity_states": {},
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
await self.async_scan_devices()
|
await self.async_scan_devices()
|
||||||
for key, update_fn in self._entity_update_functions.items():
|
for key, update_fn in self._entity_update_functions.items():
|
||||||
_LOGGER.debug("update entity %s", key)
|
_LOGGER.debug("update entity %s", key)
|
||||||
enity_data[key] = await self.hass.async_add_executor_job(
|
entity_data["entity_states"][
|
||||||
|
key
|
||||||
|
] = await self.hass.async_add_executor_job(
|
||||||
update_fn, self.fritz_status, self.data.get(key)
|
update_fn, self.fritz_status, self.data.get(key)
|
||||||
)
|
)
|
||||||
|
if self.has_call_deflections:
|
||||||
|
entity_data[
|
||||||
|
"call_deflections"
|
||||||
|
] = await self.async_update_call_deflections()
|
||||||
except FRITZ_EXCEPTIONS as ex:
|
except FRITZ_EXCEPTIONS as ex:
|
||||||
raise update_coordinator.UpdateFailed(ex) from ex
|
raise update_coordinator.UpdateFailed(ex) from ex
|
||||||
_LOGGER.debug("enity_data: %s", enity_data)
|
|
||||||
return enity_data
|
_LOGGER.debug("enity_data: %s", entity_data)
|
||||||
|
return entity_data
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
@ -354,6 +375,22 @@ class FritzBoxTools(
|
|||||||
"""Retrieve latest device information from the FRITZ!Box."""
|
"""Retrieve latest device information from the FRITZ!Box."""
|
||||||
return await self.hass.async_add_executor_job(self._update_device_info)
|
return await self.hass.async_add_executor_job(self._update_device_info)
|
||||||
|
|
||||||
|
async def async_update_call_deflections(
|
||||||
|
self,
|
||||||
|
) -> dict[int, dict[str, Any]]:
|
||||||
|
"""Call GetDeflections action from X_AVM-DE_OnTel service."""
|
||||||
|
raw_data = await self.hass.async_add_executor_job(
|
||||||
|
partial(self.connection.call_action, "X_AVM-DE_OnTel1", "GetDeflections")
|
||||||
|
)
|
||||||
|
if not raw_data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
items = xmltodict.parse(raw_data["NewDeflectionList"])["List"]["Item"]
|
||||||
|
if not isinstance(items, list):
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
return {int(item["DeflectionId"]): item for item in items}
|
||||||
|
|
||||||
async def _async_get_wan_access(self, ip_address: str) -> bool | None:
|
async def _async_get_wan_access(self, ip_address: str) -> bool | None:
|
||||||
"""Get WAN access rule for given IP address."""
|
"""Get WAN access rule for given IP address."""
|
||||||
try:
|
try:
|
||||||
@ -772,18 +809,6 @@ class AvmWrapper(FritzBoxTools):
|
|||||||
"WLANConfiguration", str(index), "GetInfo"
|
"WLANConfiguration", str(index), "GetInfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_get_ontel_num_deflections(self) -> dict[str, Any]:
|
|
||||||
"""Call GetNumberOfDeflections action from X_AVM-DE_OnTel service."""
|
|
||||||
|
|
||||||
return await self._async_service_call(
|
|
||||||
"X_AVM-DE_OnTel", "1", "GetNumberOfDeflections"
|
|
||||||
)
|
|
||||||
|
|
||||||
async def async_get_ontel_deflections(self) -> dict[str, Any]:
|
|
||||||
"""Call GetDeflections action from X_AVM-DE_OnTel service."""
|
|
||||||
|
|
||||||
return await self._async_service_call("X_AVM-DE_OnTel", "1", "GetDeflections")
|
|
||||||
|
|
||||||
async def async_set_wlan_configuration(
|
async def async_set_wlan_configuration(
|
||||||
self, index: int, turn_on: bool
|
self, index: int, turn_on: bool
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
|
@ -309,4 +309,4 @@ class FritzBoxSensor(FritzBoxBaseCoordinatorEntity, SensorEntity):
|
|||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the value reported by the sensor."""
|
"""Return the value reported by the sensor."""
|
||||||
return self.coordinator.data.get(self.entity_description.key)
|
return self.coordinator.data["entity_states"].get(self.entity_description.key)
|
||||||
|
@ -4,10 +4,8 @@ from __future__ import annotations
|
|||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import xmltodict
|
|
||||||
|
|
||||||
from homeassistant.components.network import async_get_source_ip
|
from homeassistant.components.network import async_get_source_ip
|
||||||
from homeassistant.components.switch import SwitchEntity
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
@ -15,6 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import slugify
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .common import (
|
from .common import (
|
||||||
@ -47,31 +46,15 @@ async def _async_deflection_entities_list(
|
|||||||
|
|
||||||
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION)
|
_LOGGER.debug("Setting up %s switches", SWITCH_TYPE_DEFLECTION)
|
||||||
|
|
||||||
deflections_response = await avm_wrapper.async_get_ontel_num_deflections()
|
if (
|
||||||
if not deflections_response:
|
call_deflections := avm_wrapper.data.get("call_deflections")
|
||||||
|
) is None or not isinstance(call_deflections, dict):
|
||||||
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
|
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Specific %s response: GetNumberOfDeflections=%s",
|
|
||||||
SWITCH_TYPE_DEFLECTION,
|
|
||||||
deflections_response,
|
|
||||||
)
|
|
||||||
|
|
||||||
if deflections_response["NewNumberOfDeflections"] == 0:
|
|
||||||
_LOGGER.debug("The FRITZ!Box has no %s options", SWITCH_TYPE_DEFLECTION)
|
|
||||||
return []
|
|
||||||
|
|
||||||
if not (deflection_list := await avm_wrapper.async_get_ontel_deflections()):
|
|
||||||
return []
|
|
||||||
|
|
||||||
items = xmltodict.parse(deflection_list["NewDeflectionList"])["List"]["Item"]
|
|
||||||
if not isinstance(items, list):
|
|
||||||
items = [items]
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
FritzBoxDeflectionSwitch(avm_wrapper, device_friendly_name, dict_of_deflection)
|
FritzBoxDeflectionSwitch(avm_wrapper, device_friendly_name, cd_id)
|
||||||
for dict_of_deflection in items
|
for cd_id in call_deflections
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -273,6 +256,61 @@ async def async_setup_entry(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FritzBoxBaseCoordinatorSwitch(CoordinatorEntity, SwitchEntity):
|
||||||
|
"""Fritz switch coordinator base class."""
|
||||||
|
|
||||||
|
coordinator: AvmWrapper
|
||||||
|
entity_description: SwitchEntityDescription
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
avm_wrapper: AvmWrapper,
|
||||||
|
device_name: str,
|
||||||
|
description: SwitchEntityDescription,
|
||||||
|
) -> None:
|
||||||
|
"""Init device info class."""
|
||||||
|
super().__init__(avm_wrapper)
|
||||||
|
self.entity_description = description
|
||||||
|
self._device_name = device_name
|
||||||
|
self._attr_unique_id = f"{avm_wrapper.unique_id}-{description.key}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self) -> DeviceInfo:
|
||||||
|
"""Return the device information."""
|
||||||
|
return DeviceInfo(
|
||||||
|
configuration_url=f"http://{self.coordinator.host}",
|
||||||
|
connections={(CONNECTION_NETWORK_MAC, self.coordinator.mac)},
|
||||||
|
identifiers={(DOMAIN, self.coordinator.unique_id)},
|
||||||
|
manufacturer="AVM",
|
||||||
|
model=self.coordinator.model,
|
||||||
|
name=self._device_name,
|
||||||
|
sw_version=self.coordinator.current_firmware,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data(self) -> dict[str, Any]:
|
||||||
|
"""Return entity data from coordinator data."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return availability based on data availability."""
|
||||||
|
return super().available and bool(self.data)
|
||||||
|
|
||||||
|
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||||
|
"""Handle switch state change request."""
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn on switch."""
|
||||||
|
await self._async_handle_turn_on_off(turn_on=True)
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off switch."""
|
||||||
|
await self._async_handle_turn_on_off(turn_on=False)
|
||||||
|
|
||||||
|
|
||||||
class FritzBoxBaseSwitch(FritzBoxBaseEntity):
|
class FritzBoxBaseSwitch(FritzBoxBaseEntity):
|
||||||
"""Fritz switch base class."""
|
"""Fritz switch base class."""
|
||||||
|
|
||||||
@ -417,69 +455,51 @@ class FritzBoxPortSwitch(FritzBoxBaseSwitch, SwitchEntity):
|
|||||||
return bool(resp is not None)
|
return bool(resp is not None)
|
||||||
|
|
||||||
|
|
||||||
class FritzBoxDeflectionSwitch(FritzBoxBaseSwitch, SwitchEntity):
|
class FritzBoxDeflectionSwitch(FritzBoxBaseCoordinatorSwitch):
|
||||||
"""Defines a FRITZ!Box Tools PortForward switch."""
|
"""Defines a FRITZ!Box Tools PortForward switch."""
|
||||||
|
|
||||||
|
_attr_entity_category = EntityCategory.CONFIG
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
avm_wrapper: AvmWrapper,
|
avm_wrapper: AvmWrapper,
|
||||||
device_friendly_name: str,
|
device_friendly_name: str,
|
||||||
dict_of_deflection: Any,
|
deflection_id: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Init Fritxbox Deflection class."""
|
"""Init Fritxbox Deflection class."""
|
||||||
self._avm_wrapper = avm_wrapper
|
self.deflection_id = deflection_id
|
||||||
|
description = SwitchEntityDescription(
|
||||||
self.dict_of_deflection = dict_of_deflection
|
key=f"call_deflection_{self.deflection_id}",
|
||||||
self._attributes = {}
|
name=f"Call deflection {self.deflection_id}",
|
||||||
self.id = int(self.dict_of_deflection["DeflectionId"])
|
|
||||||
self._attr_entity_category = EntityCategory.CONFIG
|
|
||||||
|
|
||||||
switch_info = SwitchInfo(
|
|
||||||
description=f"Call deflection {self.id}",
|
|
||||||
friendly_name=device_friendly_name,
|
|
||||||
icon="mdi:phone-forward",
|
icon="mdi:phone-forward",
|
||||||
type=SWITCH_TYPE_DEFLECTION,
|
|
||||||
callback_update=self._async_fetch_update,
|
|
||||||
callback_switch=self._async_switch_on_off_executor,
|
|
||||||
)
|
)
|
||||||
super().__init__(self._avm_wrapper, device_friendly_name, switch_info)
|
super().__init__(avm_wrapper, device_friendly_name, description)
|
||||||
|
|
||||||
async def _async_fetch_update(self) -> None:
|
@property
|
||||||
"""Fetch updates."""
|
def data(self) -> dict[str, Any]:
|
||||||
|
"""Return call deflection data."""
|
||||||
|
return self.coordinator.data["call_deflections"].get(self.deflection_id, {})
|
||||||
|
|
||||||
resp = await self._avm_wrapper.async_get_ontel_deflections()
|
@property
|
||||||
if not resp:
|
def extra_state_attributes(self) -> dict[str, str]:
|
||||||
self._is_available = False
|
"""Return device attributes."""
|
||||||
return
|
return {
|
||||||
|
"type": self.data["Type"],
|
||||||
|
"number": self.data["Number"],
|
||||||
|
"deflection_to_number": self.data["DeflectionToNumber"],
|
||||||
|
"mode": self.data["Mode"][1:],
|
||||||
|
"outgoing": self.data["Outgoing"],
|
||||||
|
"phonebook_id": self.data["PhonebookID"],
|
||||||
|
}
|
||||||
|
|
||||||
self.dict_of_deflection = xmltodict.parse(resp["NewDeflectionList"])["List"][
|
@property
|
||||||
"Item"
|
def is_on(self) -> bool | None:
|
||||||
]
|
"""Switch status."""
|
||||||
if isinstance(self.dict_of_deflection, list):
|
return self.data.get("Enable") == "1"
|
||||||
self.dict_of_deflection = self.dict_of_deflection[self.id]
|
|
||||||
|
|
||||||
_LOGGER.debug(
|
async def _async_handle_turn_on_off(self, turn_on: bool) -> None:
|
||||||
"Specific %s response: NewDeflectionList=%s",
|
|
||||||
SWITCH_TYPE_DEFLECTION,
|
|
||||||
self.dict_of_deflection,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._attr_is_on = self.dict_of_deflection["Enable"] == "1"
|
|
||||||
self._is_available = True
|
|
||||||
|
|
||||||
self._attributes["type"] = self.dict_of_deflection["Type"]
|
|
||||||
self._attributes["number"] = self.dict_of_deflection["Number"]
|
|
||||||
self._attributes["deflection_to_number"] = self.dict_of_deflection[
|
|
||||||
"DeflectionToNumber"
|
|
||||||
]
|
|
||||||
# Return mode sample: "eImmediately"
|
|
||||||
self._attributes["mode"] = self.dict_of_deflection["Mode"][1:]
|
|
||||||
self._attributes["outgoing"] = self.dict_of_deflection["Outgoing"]
|
|
||||||
self._attributes["phonebook_id"] = self.dict_of_deflection["PhonebookID"]
|
|
||||||
|
|
||||||
async def _async_switch_on_off_executor(self, turn_on: bool) -> None:
|
|
||||||
"""Handle deflection switch."""
|
"""Handle deflection switch."""
|
||||||
await self._avm_wrapper.async_set_deflection_enable(self.id, turn_on)
|
await self.coordinator.async_set_deflection_enable(self.deflection_id, turn_on)
|
||||||
|
|
||||||
|
|
||||||
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
class FritzBoxProfileSwitch(FritzDeviceBase, SwitchEntity):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user