Skip to content

Commit 784b377

Browse files
authored
Add CPU/memory sensors and polling interval control (#504)
Adds extended diagnostics and polling control improvements to the TP-Link Deco integration. ### Added - CPU usage sensors (raw and smoothed) - Memory usage sensors (raw and smoothed) - Configurable polling interval (10 / 30 / 60 / 120 seconds) - Select entity to control polling interval from the Home Assistant UI ### Improvements - Fixed config entry reload handling when changing options - Improved stability of polling and API interaction ### Existing functionality - Pause / resume polling via services - Polling control switch This improves both usability and stability, while adding useful diagnostics for monitoring Deco performance.
1 parent e121d7f commit 784b377

7 files changed

Lines changed: 180 additions & 12 deletions

File tree

custom_components/tplink_deco/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,20 @@ async def handle_resume_polling(service: ServiceCall) -> None:
259259
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
260260
"""Handle removal of an entry."""
261261
_LOGGER.debug("async_unload_entry: Config entry %s", config_entry.entry_id)
262-
data = hass.data[DOMAIN][config_entry.entry_id]
262+
263+
domain_data = hass.data.get(DOMAIN, {})
264+
data = domain_data.get(config_entry.entry_id)
265+
266+
if data is None:
267+
_LOGGER.debug(
268+
"async_unload_entry: No stored data for config entry %s",
269+
config_entry.entry_id,
270+
)
271+
return True
272+
263273
deco_coordinator = data.get(COORDINATOR_DECOS_KEY)
264274
clients_coordinator = data.get(COORDINATOR_CLIENTS_KEY)
275+
265276
if deco_coordinator is not None:
266277
await deco_coordinator.async_close()
267278
if clients_coordinator is not None:
@@ -275,24 +286,19 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
275286
]
276287
)
277288
)
289+
278290
if unloaded:
279-
hass.data[DOMAIN].pop(config_entry.entry_id)
291+
hass.data[DOMAIN].pop(config_entry.entry_id, None)
280292
hass.services.async_remove(DOMAIN, SERVICE_PAUSE_POLLING)
281293
hass.services.async_remove(DOMAIN, SERVICE_RESUME_POLLING)
282-
return unloaded
283294

284-
285-
async def async_reload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
286-
"""Reload config entry."""
287-
_LOGGER.debug("async_reload_entry: Config entry %s", config_entry)
288-
await async_unload_entry(hass, config_entry)
289-
await async_setup_entry(hass, config_entry)
295+
return unloaded
290296

291297

292298
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
293299
"""Update options."""
294300
_LOGGER.debug("update_listener: Reloading %s", config_entry.entry_id)
295-
await async_reload_entry(hass, config_entry)
301+
await hass.config_entries.async_reload(config_entry.entry_id)
296302

297303

298304
async def async_migrate_entry(hass, config_entry: ConfigEntry):

custom_components/tplink_deco/api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,27 @@ async def async_reboot_decos(self, deco_macs) -> dict:
212212
check_data_error_code(context, data)
213213
_LOGGER.debug("Rebooted decos %s", deco_macs)
214214

215+
# Return performance data (CPU / memory)
216+
async def async_get_performance(self) -> dict:
217+
return await self._async_call_with_retry(self._async_get_performance)
218+
219+
async def _async_get_performance(self) -> dict:
220+
await self.async_login_if_needed()
221+
222+
context = "Get Performance"
223+
performance_payload = {"operation": "read"}
224+
225+
response_json = await self._async_post(
226+
context,
227+
f"{self._host}/cgi-bin/luci/;stok={self._stok}/admin/network",
228+
params={"form": "performance"},
229+
data=self._encode_payload(performance_payload),
230+
)
231+
232+
data = self._decrypt_data(context, response_json["data"])
233+
check_data_error_code(context, data)
234+
return data
235+
215236
# Return list of clients. Default lists clients for all decos.
216237
async def async_list_clients(self, deco_mac="default") -> dict:
217238
return await self._async_call_with_retry(self._async_list_clients, deco_mac)

custom_components/tplink_deco/config_flow.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ def _get_schema(data: dict[str:Any]):
5252
data = {}
5353
schema = _get_auth_schema(data)
5454
scan_interval = data.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
55+
56+
if scan_interval not in [10, 30, 60, 120]:
57+
scan_interval = DEFAULT_SCAN_INTERVAL
58+
5559
schema.update(
5660
{
5761
vol.Required(
@@ -60,7 +64,7 @@ def _get_schema(data: dict[str:Any]):
6064
vol.Required(
6165
CONF_SCAN_INTERVAL,
6266
default=scan_interval,
63-
): vol.All(vol.Coerce(int), vol.Range(min=1)),
67+
): vol.In([10, 30, 60, 120]),
6468
vol.Required(
6569
CONF_CONSIDER_HOME,
6670
default=data.get(CONF_CONSIDER_HOME, DEFAULT_CONSIDER_HOME),

custom_components/tplink_deco/const.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@
5353
SERVICE_PAUSE_POLLING = "pause_polling"
5454
SERVICE_RESUME_POLLING = "resume_polling"
5555
# Platforms
56-
PLATFORMS = ["device_tracker", "sensor", "binary_sensor", "switch"]
56+
PLATFORMS = ["device_tracker", "sensor", "binary_sensor", "switch", "select"]

custom_components/tplink_deco/coordinator.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def __init__(self, mac: str) -> None:
7474
self.signal_band5 = None
7575
self.backhaul_speed = None
7676
self.backhaul_max_speed = None
77+
self.cpu_usage = None
78+
self.cpu_usage_raw = None
79+
self.mem_usage = None
80+
self.mem_usage_raw = None
7781

7882
def update(
7983
self,
@@ -187,6 +191,10 @@ async def _async_update_data(self):
187191
self.api.async_list_devices
188192
)
189193

194+
performance_data = await async_call_and_propagate_config_error(
195+
self.api.async_get_performance
196+
)
197+
190198
old_decos = self.data.decos
191199
master_deco = None
192200
deco_added = False
@@ -203,6 +211,34 @@ async def _async_update_data(self):
203211
if deco.master:
204212
master_deco = deco
205213

214+
# Zet globale performance data op de master Deco
215+
result = performance_data.get("result", {})
216+
if master_deco is not None:
217+
cpu_raw = result.get("cpu_usage")
218+
mem_raw = result.get("mem_usage")
219+
220+
if cpu_raw is not None:
221+
cpu_percent = cpu_raw * 100
222+
master_deco.cpu_usage_raw = round(cpu_percent, 1)
223+
224+
if master_deco.cpu_usage is not None:
225+
master_deco.cpu_usage = round(
226+
(master_deco.cpu_usage * 0.7) + (cpu_percent * 0.3), 1
227+
)
228+
else:
229+
master_deco.cpu_usage = round(cpu_percent, 1)
230+
231+
if mem_raw is not None:
232+
mem_percent = mem_raw * 100
233+
master_deco.mem_usage_raw = round(mem_percent, 1)
234+
235+
if master_deco.mem_usage is not None:
236+
master_deco.mem_usage = round(
237+
(master_deco.mem_usage * 0.7) + (mem_percent * 0.3), 1
238+
)
239+
else:
240+
master_deco.mem_usage = round(mem_percent, 1)
241+
206242
if deco_added:
207243
async_dispatcher_send(self.hass, SIGNAL_DECO_ADDED)
208244

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from homeassistant.components.device_tracker.const import CONF_SCAN_INTERVAL
2+
from homeassistant.components.select import SelectEntity
3+
from homeassistant.config_entries import ConfigEntry
4+
from homeassistant.core import HomeAssistant
5+
from homeassistant.helpers.entity import DeviceInfo
6+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
7+
8+
from .const import COORDINATOR_DECOS_KEY
9+
from .const import DOMAIN
10+
from .device import create_device_info
11+
12+
POLLING_INTERVAL_OPTIONS = ["10", "30", "60", "120"]
13+
14+
15+
async def async_setup_entry(
16+
hass: HomeAssistant,
17+
config_entry: ConfigEntry,
18+
async_add_entities: AddEntitiesCallback,
19+
):
20+
"""Set up select entities."""
21+
coordinator = hass.data[DOMAIN][config_entry.entry_id][COORDINATOR_DECOS_KEY]
22+
async_add_entities([DecoPollingIntervalSelect(hass, config_entry, coordinator)])
23+
24+
25+
class DecoPollingIntervalSelect(SelectEntity):
26+
"""Select entity to control Deco polling interval."""
27+
28+
_attr_has_entity_name = True
29+
_attr_name = "Polling interval"
30+
_attr_icon = "mdi:timer-cog"
31+
_attr_options = POLLING_INTERVAL_OPTIONS
32+
33+
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry, coordinator):
34+
self.hass = hass
35+
self.config_entry = config_entry
36+
self.coordinator = coordinator
37+
self._attr_unique_id = f"{config_entry.entry_id}_polling_interval"
38+
39+
@property
40+
def current_option(self) -> str:
41+
"""Return current polling interval as string."""
42+
value = self.config_entry.data.get(CONF_SCAN_INTERVAL, 30)
43+
return str(value)
44+
45+
@property
46+
def device_info(self) -> DeviceInfo | None:
47+
"""Attach select to the master Deco device."""
48+
master_deco = self.coordinator.data.master_deco
49+
if master_deco is None:
50+
return None
51+
return create_device_info(master_deco, master_deco)
52+
53+
async def async_select_option(self, option: str) -> None:
54+
"""Change polling interval."""
55+
new_value = int(option)
56+
57+
data = dict(self.config_entry.data)
58+
data[CONF_SCAN_INTERVAL] = new_value
59+
60+
self.hass.config_entries.async_update_entry(
61+
self.config_entry,
62+
data=data,
63+
)
64+
65+
await self.hass.config_entries.async_reload(self.config_entry.entry_id)

custom_components/tplink_deco/sensor.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,42 @@ class TplinkDecoDiagnosticSensorDescription(SensorEntityDescription):
8686
native_unit_of_measurement=UnitOfDataRate.MEGABITS_PER_SECOND,
8787
value_fn=lambda deco: deco.backhaul_max_speed,
8888
),
89+
TplinkDecoDiagnosticSensorDescription(
90+
key="cpu_usage",
91+
name="CPU usage",
92+
entity_category=EntityCategory.DIAGNOSTIC,
93+
entity_registry_enabled_default=False,
94+
native_unit_of_measurement="%",
95+
state_class=SensorStateClass.MEASUREMENT,
96+
value_fn=lambda deco: deco.cpu_usage,
97+
),
98+
TplinkDecoDiagnosticSensorDescription(
99+
key="cpu_usage_raw",
100+
name="CPU usage raw",
101+
entity_category=EntityCategory.DIAGNOSTIC,
102+
entity_registry_enabled_default=False,
103+
native_unit_of_measurement="%",
104+
state_class=SensorStateClass.MEASUREMENT,
105+
value_fn=lambda deco: deco.cpu_usage_raw,
106+
),
107+
TplinkDecoDiagnosticSensorDescription(
108+
key="mem_usage",
109+
name="Memory usage",
110+
entity_category=EntityCategory.DIAGNOSTIC,
111+
entity_registry_enabled_default=False,
112+
native_unit_of_measurement="%",
113+
state_class=SensorStateClass.MEASUREMENT,
114+
value_fn=lambda deco: deco.mem_usage,
115+
),
116+
TplinkDecoDiagnosticSensorDescription(
117+
key="mem_usage_raw",
118+
name="Memory usage raw",
119+
entity_category=EntityCategory.DIAGNOSTIC,
120+
entity_registry_enabled_default=False,
121+
native_unit_of_measurement="%",
122+
state_class=SensorStateClass.MEASUREMENT,
123+
value_fn=lambda deco: deco.mem_usage_raw,
124+
),
89125
)
90126

91127

0 commit comments

Comments
 (0)