1 """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router."""
3 from __future__
import annotations
5 from collections.abc
import Mapping
6 from datetime
import datetime, timedelta
7 from enum
import StrEnum
10 from threading
import Event
as ThreadingEvent, Thread
11 from time
import sleep
12 from typing
import Any, cast
14 from fritzconnection.core.fritzmonitor
import FritzMonitor
22 from .
import FritzBoxCallMonitorConfigEntry
23 from .base
import FritzBoxPhonebook
34 _LOGGER = logging.getLogger(__name__)
40 """Fritz sensor call states."""
50 config_entry: FritzBoxCallMonitorConfigEntry,
51 async_add_entities: AddEntitiesCallback,
53 """Set up the fritzbox_callmonitor sensor from config_entry."""
54 fritzbox_phonebook = config_entry.runtime_data
56 phonebook_id: int = config_entry.data[CONF_PHONEBOOK]
57 prefixes: list[str] |
None = config_entry.options.get(CONF_PREFIXES)
58 serial_number: str = config_entry.data[SERIAL_NUMBER]
59 host: str = config_entry.data[CONF_HOST]
60 port: int = config_entry.data[CONF_PORT]
62 unique_id = f
"{serial_number}-{phonebook_id}"
65 phonebook_name=config_entry.title,
67 fritzbox_phonebook=fritzbox_phonebook,
77 """Implementation of a Fritz!Box call monitor."""
79 _attr_has_entity_name =
True
80 _attr_translation_key = DOMAIN
81 _attr_device_class = SensorDeviceClass.ENUM
82 _attr_options =
list(CallState)
88 fritzbox_phonebook: FritzBoxPhonebook,
89 prefixes: list[str] |
None,
93 """Initialize the sensor."""
98 self.
_monitor_monitor: FritzBoxCallMonitor |
None =
None
99 self.
_attributes_attributes: dict[str, str | list[str]] = {}
106 identifiers={(DOMAIN, unique_id)},
107 manufacturer=MANUFACTURER,
114 """Connect to FRITZ!Box to monitor its call state."""
118 self.
hasshass.bus.async_listen_once(
124 """Disconnect from FRITZ!Box by stopping monitor."""
129 """Check connection and start callmonitor thread."""
130 _LOGGER.debug(
"Starting monitor for: %s", self.
entity_identity_id)
132 host=self.
_host_host,
133 port=self.
_port_port,
139 """Stop callmonitor thread."""
143 and not self.
_monitor_monitor.stopped.is_set()
144 and self.
_monitor_monitor.connection
145 and self.
_monitor_monitor.connection.is_alive
148 self.
_monitor_monitor.connection.stop()
149 _LOGGER.debug(
"Stopped monitor for: %s", self.
entity_identity_id)
156 """Set the state attributes."""
161 """Return the state attributes."""
167 """Return a name for a given phone number."""
171 """Update the phonebook if it is defined."""
176 """Event listener to monitor calls on the Fritz!Box."""
178 def __init__(self, host: str, port: int, sensor: FritzBoxCallSensor) ->
None:
179 """Initialize Fritz!Box monitor instance."""
182 self.
connectionconnection: FritzMonitor |
None =
None
187 """Connect to the Fritz!Box."""
188 _LOGGER.debug(
"Setting up socket connection")
191 kwargs: dict[str, Any] = {
192 "event_queue": self.
connectionconnection.start(
193 reconnect_tries=50, reconnect_delay=120
196 Thread(target=self.
_process_events_process_events, kwargs=kwargs).start()
197 except OSError
as err:
200 "Cannot connect to %s on port %s: %s", self.
hosthost, self.
portport, err
204 """Listen to incoming or outgoing calls."""
205 _LOGGER.debug(
"Connection established, waiting for events")
206 while not self.
stoppedstopped.is_set():
208 event = event_queue.get(timeout=10)
211 not cast(FritzMonitor, self.
connectionconnection).is_alive
212 and not self.
stoppedstopped.is_set()
214 _LOGGER.error(
"Connection has abruptly ended")
215 _LOGGER.debug(
"Empty event queue")
218 _LOGGER.debug(
"Received event: %s", event)
223 """Parse the call information and set the sensor states."""
224 line = event.split(
";")
225 df_in =
"%d.%m.%y %H:%M:%S"
226 df_out =
"%Y-%m-%dT%H:%M:%S"
227 isotime = datetime.strptime(line[0], df_in).strftime(df_out)
228 if line[1] == FritzState.RING:
229 self.
_sensor_sensor.set_state(CallState.RINGING)
235 "initiated": isotime,
236 "from_name": self.
_sensor_sensor.number_to_name(line[3]),
238 self.
_sensor_sensor.set_attributes(att)
239 elif line[1] == FritzState.CALL:
240 self.
_sensor_sensor.set_state(CallState.DIALING)
246 "initiated": isotime,
247 "to_name": self.
_sensor_sensor.number_to_name(line[5]),
249 self.
_sensor_sensor.set_attributes(att)
250 elif line[1] == FritzState.CONNECT:
251 self.
_sensor_sensor.set_state(CallState.TALKING)
256 "with_name": self.
_sensor_sensor.number_to_name(line[4]),
258 self.
_sensor_sensor.set_attributes(att)
259 elif line[1] == FritzState.DISCONNECT:
260 self.
_sensor_sensor.set_state(CallState.IDLE)
261 att = {
"duration": line[3],
"closed": isotime}
262 self.
_sensor_sensor.set_attributes(att)
263 self.
_sensor_sensor.schedule_update_ha_state()
None _parse(self, str event)
None _process_events(self, queue.Queue[str] event_queue)
None __init__(self, str host, int port, FritzBoxCallSensor sensor)
None _stop_call_monitor(self, Event|None event=None)
None set_attributes(self, Mapping[str, str] attributes)
None async_will_remove_from_hass(self)
str number_to_name(self, str number)
None __init__(self, str phonebook_name, str unique_id, FritzBoxPhonebook fritzbox_phonebook, list[str]|None prefixes, str host, int port)
None set_state(self, CallState state)
dict[str, str|list[str]] extra_state_attributes(self)
_attr_translation_placeholders
None async_added_to_hass(self)
None _start_call_monitor(self)
None async_on_remove(self, CALLBACK_TYPE func)
str get_name(AirthingsDevice device)
None async_setup_entry(HomeAssistant hass, FritzBoxCallMonitorConfigEntry config_entry, AddEntitiesCallback async_add_entities)