• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

openwisp / openwisp-monitoring / 29622336079

18 Jul 2026 12:06AM UTC coverage: 98.588% (+0.002%) from 98.586%
29622336079

Pull #832

github

web-flow
Merge 28d967222 into 30fc297ca
Pull Request #832: [backport] #831: [fix] Fixed devices stuck in PROBLEM status #830 (to 1.2)

3840 of 3895 relevant lines covered (98.59%)

11.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.87
openwisp_monitoring/device/base/models.py
1
import json
12✔
2
import random
12✔
3
from collections import OrderedDict
12✔
4
from datetime import datetime
12✔
5

6
import swapper
12✔
7
from cache_memoize import cache_memoize
12✔
8
from dateutil.relativedelta import relativedelta
12✔
9
from django.core.cache import cache
12✔
10
from django.core.exceptions import ValidationError
12✔
11
from django.db import models
12✔
12
from django.db.models.signals import post_delete
12✔
13
from django.dispatch import receiver
12✔
14
from django.utils.module_loading import import_string
12✔
15
from django.utils.timezone import now
12✔
16
from django.utils.translation import gettext_lazy as _
12✔
17
from jsonschema import draft7_format_checker, validate
12✔
18
from jsonschema.exceptions import ValidationError as SchemaError
12✔
19
from model_utils import Choices
12✔
20
from model_utils.fields import StatusField
12✔
21
from netaddr import EUI, NotRegisteredError
12✔
22
from pytz import timezone as tz
12✔
23
from swapper import load_model
12✔
24

25
from openwisp_controller.config.validators import mac_address_validator
12✔
26
from openwisp_utils.base import TimeStampedEditableModel
12✔
27

28
from ...check import settings as check_settings
12✔
29
from ...db import device_data_query, timeseries_db
12✔
30
from ...monitoring.signals import threshold_crossed
12✔
31
from ...monitoring.tasks import _timeseries_write
12✔
32
from ...settings import CACHE_TIMEOUT
12✔
33
from .. import settings as app_settings
12✔
34
from .. import tasks
12✔
35
from ..schema import schema
12✔
36
from ..signals import health_status_changed
12✔
37
from ..utils import SHORT_RP, get_device_cache_key
12✔
38

39

40
def mac_lookup_cache_timeout():
12✔
41
    """Returns a random number of hours between 48 and 96.
42

43
    This avoids timing out the entire cache at the same time.
44
    """
45
    return 60 * 60 * random.randint(48, 96)
12✔
46

47

48
class AbstractDeviceData(object):
12✔
49
    schema = schema
12✔
50
    __data = None
12✔
51
    __key = "device_data"
12✔
52
    __data_timestamp = None
12✔
53

54
    def __init__(self, *args, **kwargs):
12✔
55
        from ..writer import DeviceDataWriter
12✔
56

57
        self.data = kwargs.pop("data", None)
12✔
58
        self.writer = DeviceDataWriter(self)
12✔
59
        super().__init__(*args, **kwargs)
12✔
60

61
    @classmethod
12✔
62
    @cache_memoize(CACHE_TIMEOUT)
12✔
63
    def get_devicedata(cls, pk):
12✔
64
        obj = (
12✔
65
            cls.objects.select_related("devicelocation")
66
            .only(
67
                "id",
68
                "organization_id",
69
                "devicelocation__location_id",
70
                "devicelocation__floorplan_id",
71
            )
72
            .get(id=pk)
73
        )
74
        return obj
12✔
75

76
    @classmethod
12✔
77
    def invalidate_cache(cls, instance, *args, **kwargs):
12✔
78
        if isinstance(instance, load_model("geo", "DeviceLocation")):
12✔
79
            pk = instance.content_object_id
12✔
80
        else:
81
            if kwargs.get("created"):
12✔
82
                return
12✔
83
            pk = instance.pk
12✔
84
        cls.get_devicedata.invalidate(cls, str(pk))
12✔
85

86
    def can_be_updated(self):
12✔
87
        """Do not attempt to push the conf if the device is not reachable."""
88
        can_be_updated = super().can_be_updated()
12✔
89
        return can_be_updated and self.monitoring.status not in ["critical", "unknown"]
12✔
90

91
    def _get_wifi_version(self, htmode):
12✔
92
        wifi_version_htmode = f'{_("Other")}: {htmode}'
12✔
93
        if "NOHT" in htmode:
12✔
94
            wifi_version_htmode = f'{_("Legacy Mode")}: {htmode}'
12✔
95
        elif "HE" in htmode:
12✔
96
            wifi_version_htmode = f"WiFi 6 (802.11ax): {htmode}"
12✔
97
        elif "VHT" in htmode:
12✔
98
            wifi_version_htmode = f"WiFi 5 (802.11ac): {htmode}"
12✔
99
        elif "HT" in htmode:
12✔
100
            wifi_version_htmode = f"WiFi 4 (802.11n): {htmode}"
12✔
101
        return wifi_version_htmode
12✔
102

103
    @property
12✔
104
    def data_user_friendly(self):
12✔
105
        if not self.data:
12✔
106
            return None
12✔
107
        data = self.data
12✔
108
        # slicing to eliminate the nanoseconds from timestamp
109
        measured_at = datetime.strptime(self.data_timestamp[0:19], "%Y-%m-%dT%H:%M:%S")
12✔
110
        time_elapsed = int((datetime.utcnow() - measured_at).total_seconds())
12✔
111
        if "general" in data and "local_time" in data["general"]:
12✔
112
            local_time = data["general"]["local_time"]
12✔
113
            data["general"]["local_time"] = datetime.fromtimestamp(
12✔
114
                local_time + time_elapsed, tz=tz("UTC")
115
            )
116
        if "general" in data and "uptime" in data["general"]:
12✔
117
            uptime = "{0.days} days, {0.hours} hours and {0.minutes} minutes"
12✔
118
            data["general"]["uptime"] = uptime.format(
12✔
119
                relativedelta(seconds=data["general"]["uptime"] + time_elapsed)
120
            )
121
        # used for reordering interfaces
122
        interface_dict = OrderedDict()
12✔
123
        for interface in data.get("interfaces", []):
12✔
124
            # don't show interfaces if they don't have any useful info
125
            if len(interface.keys()) <= 2:
12✔
126
                continue
12✔
127
            # human readable wireless  mode
128
            if "wireless" in interface and "mode" in interface["wireless"]:
12✔
129
                interface["wireless"]["mode"] = interface["wireless"]["mode"].replace(
12✔
130
                    "_", " "
131
                )
132
            # convert to GHz
133
            if "wireless" in interface and "frequency" in interface["wireless"]:
12✔
134
                interface["wireless"]["frequency"] /= 1000
12✔
135
            # add wifi version
136
            if "wireless" in interface and "htmode" in interface["wireless"]:
12✔
137
                interface["wireless"]["htmode"] = self._get_wifi_version(
12✔
138
                    interface["wireless"]["htmode"]
139
                )
140

141
            interface_dict[interface["name"]] = interface
12✔
142
        # reorder interfaces in alphabetical order
143
        interface_dict = OrderedDict(sorted(interface_dict.items()))
12✔
144
        data["interfaces"] = list(interface_dict.values())
12✔
145
        # reformat expiry in dhcp leases
146
        for lease in data.get("dhcp_leases", []):
12✔
147
            lease["expiry"] = datetime.fromtimestamp(lease["expiry"], tz=tz("UTC"))
12✔
148
        return data
12✔
149

150
    @property
12✔
151
    def data(self):
12✔
152
        """Retrieves last data snapshot from Timeseries Database."""
153
        if self.__data:
12✔
154
            return self.__data
12✔
155
        q = device_data_query.format(SHORT_RP, self.__key, self.pk)
12✔
156
        cache_key = get_device_cache_key(device=self, context="current-data")
12✔
157
        points = cache.get(cache_key)
12✔
158
        if not points:
12✔
159
            points = timeseries_db.get_list_query(q, precision=None)
12✔
160
        if not points:
12✔
161
            return None
12✔
162
        self.data_timestamp = points[0]["time"]
12✔
163
        return json.loads(points[0]["data"])
12✔
164

165
    @data.setter
12✔
166
    def data(self, data):
12✔
167
        """Sets data."""
168
        self.__data = data
12✔
169

170
    @property
12✔
171
    def data_timestamp(self):
12✔
172
        """Retrieves timestamp at which the data was recorded."""
173
        return self.__data_timestamp
12✔
174

175
    @data_timestamp.setter
12✔
176
    def data_timestamp(self, value):
12✔
177
        """Sets the timestamp related to the data."""
178
        self.__data_timestamp = value
12✔
179

180
    def validate_data(self):
12✔
181
        """Validates data according to NetJSON DeviceMonitoring schema."""
182
        try:
12✔
183
            validate(self.data, self.schema, format_checker=draft7_format_checker)
12✔
184
        except SchemaError as e:
12✔
185
            path = [str(el) for el in e.path]
12✔
186
            trigger = "/".join(path)
12✔
187
            message = 'Invalid data in "#/{0}", ' "validator says:\n\n{1}".format(
12✔
188
                trigger, e.message
189
            )
190
            raise ValidationError(message)
12✔
191

192
    def _transform_data(self):
12✔
193
        """Performs corrections or additions to the device data."""
194
        mac_detection = app_settings.MAC_VENDOR_DETECTION
12✔
195
        for interface in self.data.get("interfaces", []):
12✔
196
            # loop over mobile signal values to convert them to float
197
            if "mobile" in interface and "signal" in interface["mobile"]:
12✔
198
                for signal_key, signal_values in interface["mobile"]["signal"].items():
12✔
199
                    for key, value in signal_values.items():
12✔
200
                        signal_values[key] = float(value)
12✔
201
            # If HT/VHT/HE is not being used ie. htmode = 'NOHT',
202
            # set the HT/VHT/HE field of WiFi clients to None.
203
            # This is necessary because some clients may be
204
            # VHT capable but VHT is not enabled at the radio level,
205
            # which can mislead into thinking the client is not HT/VHT/HE capable.
206
            wireless = interface.get("wireless")
12✔
207
            if wireless and all(key in wireless for key in ("htmode", "clients")):
12✔
208
                for client in wireless["clients"]:
12✔
209
                    htmode = wireless["htmode"]
12✔
210
                    ht_enabled = htmode.startswith("HT")
12✔
211
                    vht_enabled = htmode.startswith("VHT")
12✔
212
                    noht_enabled = htmode == "NOHT"
12✔
213
                    if noht_enabled:
12✔
214
                        client["ht"] = client["vht"] = None
12✔
215
                        # since 'he' field is optional
216
                        if "he" in client:
12✔
217
                            client["he"] = None
12✔
218
                    elif ht_enabled:
12✔
219
                        if client["vht"] is False:
12✔
220
                            client["vht"] = None
12✔
221
                        if client.get("he") is False:
12✔
222
                            client["he"] = None
12✔
223
                    elif vht_enabled and client.get("he") is False:
12✔
224
                        client["he"] = None
12✔
225
            # Convert bitrate from KBits/s to MBits/s
226
            if wireless and "bitrate" in wireless:
12✔
227
                interface["wireless"]["bitrate"] = round(
12✔
228
                    interface["wireless"]["bitrate"] / 1000.0, 1
229
                )
230
            # add mac vendor to wireless clients if present
231
            if (
12✔
232
                not mac_detection
233
                or "wireless" not in interface
234
                or "clients" not in interface["wireless"]
235
            ):
236
                continue
11✔
237
            for client in interface["wireless"]["clients"]:
12✔
238
                client["vendor"] = self._mac_lookup(client["mac"])
12✔
239
        if not mac_detection:
12✔
240
            return
12✔
241
        # add mac vendor to neighbors
242
        for neighbor in self.data.get("neighbors", []):
12✔
243
            # in some cases the mac_address may not be present
244
            # eg: neighbors with "FAILED" state
245
            neighbor["vendor"] = self._mac_lookup(neighbor.get("mac"))
12✔
246
        # add mac vendor to DHCP leases
247
        for lease in self.data.get("dhcp_leases", []):
12✔
248
            lease["vendor"] = self._mac_lookup(lease["mac"])
12✔
249

250
    @cache_memoize(mac_lookup_cache_timeout())
12✔
251
    def _mac_lookup(self, value):
12✔
252
        if not value:
12✔
253
            return ""
12✔
254
        try:
12✔
255
            return EUI(value).oui.registration().org
12✔
256
        except NotRegisteredError:
12✔
257
            return ""
12✔
258

259
    def save_data(self, time=None):
12✔
260
        """Validates and saves data to Timeseries Database."""
261
        self.validate_data()
12✔
262
        self._transform_data()
12✔
263
        time = time or now()
12✔
264
        options = dict(tags={"pk": self.pk}, timestamp=time, retention_policy=SHORT_RP)
12✔
265
        _timeseries_write(name=self.__key, values={"data": self.json()}, **options)
12✔
266
        cache_key = get_device_cache_key(device=self, context="current-data")
12✔
267
        # cache current data to allow getting it without querying the timeseries DB
268
        cache.set(
12✔
269
            cache_key,
270
            [
271
                {
272
                    "data": self.json(),
273
                    "time": time.astimezone(tz=tz("UTC")).isoformat(timespec="seconds"),
274
                }
275
            ],
276
            timeout=CACHE_TIMEOUT,
277
        )
278
        if app_settings.WIFI_SESSIONS_ENABLED:
12✔
279
            self.save_wifi_clients_and_sessions()
12✔
280

281
    def json(self, *args, **kwargs):
12✔
282
        return json.dumps(self.data, *args, **kwargs)
12✔
283

284
    def save_wifi_clients_and_sessions(self):
12✔
285
        _WIFICLIENT_FIELDS = ["vendor", "ht", "vht", "he", "wmm", "wds", "wps"]
12✔
286
        WifiClient = load_model("device_monitoring", "WifiClient")
12✔
287
        WifiSession = load_model("device_monitoring", "WifiSession")
12✔
288

289
        active_sessions = []
12✔
290
        interfaces = self.data.get("interfaces", [])
12✔
291
        for interface in interfaces:
12✔
292
            if interface.get("type") != "wireless":
12✔
293
                continue
12✔
294
            interface_name = interface.get("name")
12✔
295
            wireless = interface.get("wireless", {})
12✔
296
            if not wireless or wireless["mode"] != "access_point":
12✔
297
                continue
12✔
298
            ssid = wireless.get("ssid")
12✔
299
            clients = wireless.get("clients", [])
12✔
300
            for client in clients:
12✔
301
                # Save WifiClient
302
                client_obj = WifiClient.get_wifi_client(client.get("mac"))
12✔
303
                update_fields = []
12✔
304
                for field in _WIFICLIENT_FIELDS:
12✔
305
                    if getattr(client_obj, field) != client.get(field):
12✔
306
                        setattr(client_obj, field, client.get(field))
12✔
307
                        update_fields.append(field)
12✔
308
                if update_fields:
12✔
309
                    client_obj.full_clean()
12✔
310
                    client_obj.save(update_fields=update_fields)
12✔
311

312
                # Save WifiSession
313
                session_obj, _ = WifiSession.objects.get_or_create(
12✔
314
                    device_id=self.id,
315
                    interface_name=interface_name,
316
                    ssid=ssid,
317
                    wifi_client=client_obj,
318
                    stop_time=None,
319
                )
320
                active_sessions.append(session_obj.pk)
12✔
321

322
        # Close open WifiSession
323
        WifiSession.objects.filter(
12✔
324
            device_id=self.id,
325
            stop_time=None,
326
        ).exclude(
327
            pk__in=active_sessions
328
        ).update(stop_time=now())
329

330

331
class AbstractDeviceMonitoring(TimeStampedEditableModel):
12✔
332
    device = models.OneToOneField(
12✔
333
        swapper.get_model_name("config", "Device"),
334
        on_delete=models.CASCADE,
335
        related_name="monitoring",
336
    )
337
    STATUS = Choices(
12✔
338
        ("unknown", _(app_settings.HEALTH_STATUS_LABELS["unknown"])),
339
        ("ok", _(app_settings.HEALTH_STATUS_LABELS["ok"])),
340
        ("problem", _(app_settings.HEALTH_STATUS_LABELS["problem"])),
341
        ("critical", _(app_settings.HEALTH_STATUS_LABELS["critical"])),
342
        ("deactivated", _(app_settings.HEALTH_STATUS_LABELS["deactivated"])),
343
    )
344
    status = StatusField(
12✔
345
        _("health status"),
346
        db_index=True,
347
        help_text=_(
348
            '"{0}" means the device has been recently added; \n'
349
            '"{1}" means the device is operating normally; \n'
350
            '"{2}" means the device is having issues but it\'s still communicating with the server; \n'
351
            '"{3}" means the device is not communicating with the server;\n'
352
            '"{4}" means the device is deactivated;'
353
        ).format(
354
            app_settings.HEALTH_STATUS_LABELS["unknown"],
355
            app_settings.HEALTH_STATUS_LABELS["ok"],
356
            app_settings.HEALTH_STATUS_LABELS["problem"],
357
            app_settings.HEALTH_STATUS_LABELS["critical"],
358
            app_settings.HEALTH_STATUS_LABELS["deactivated"],
359
        ),
360
    )
361

362
    class Meta:
12✔
363
        abstract = True
12✔
364

365
    def update_status(self, value):
12✔
366
        # don't trigger save nor emit signal if status is not changing
367
        if self.status == value:
12✔
368
            return
12✔
369
        self.status = value
12✔
370
        self.full_clean()
12✔
371
        self.save()
12✔
372
        # clear device management_ip when device is offline
373
        if self.status == "critical" and app_settings.AUTO_CLEAR_MANAGEMENT_IP:
12✔
374
            self.device.management_ip = None
12✔
375
            self.device.save(update_fields=["management_ip"])
12✔
376

377
        health_status_changed.send(sender=self.__class__, instance=self, status=value)
12✔
378

379
    @property
12✔
380
    def related_metrics(self):
12✔
381
        Metric = load_model("monitoring", "Metric")
12✔
382
        return Metric.objects.select_related("content_type").filter(
12✔
383
            object_id=self.device_id,
384
            content_type__model="device",
385
            content_type__app_label="config",
386
        )
387

388
    @classmethod
12✔
389
    def get_active_metrics(cls):
12✔
390
        """
391
        Returns a list of active metrics (Metric.key)
392
        """
393
        if not hasattr(cls, "_active_metrics"):
12✔
394
            active_metrics = []
12✔
395
            for check in check_settings.CHECK_LIST:
12✔
396
                Check = import_string(check)
12✔
397
                active_metrics.extend(Check.get_related_metrics())
12✔
398
            cls._active_metrics = active_metrics
12✔
399
        return cls._active_metrics
12✔
400

401
    @classmethod
12✔
402
    def get_critical_checks(cls):
12✔
403
        """
404
        Returns list of critical checks.
405
        """
406
        if not hasattr(cls, "_critical_checks"):
12✔
407
            critical_checks = []
12✔
408
            for metric in app_settings.CRITICAL_DEVICE_METRICS:
12✔
409
                if metric.get("check"):
12✔
410
                    critical_checks.append(metric["check"])
12✔
411
            cls._critical_checks = critical_checks
12✔
412
        return cls._critical_checks
12✔
413

414
    @staticmethod
12✔
415
    @receiver(threshold_crossed, dispatch_uid="threshold_crossed_receiver")
12✔
416
    def threshold_crossed(sender, metric, alert_settings, target, first_time, **kwargs):
12✔
417
        """Executed when a threshold is crossed.
418

419
        Changes the health status of a device when a threshold defined in
420
        the alert settings related to the metric is crossed.
421
        """
422
        DeviceMonitoring = load_model("device_monitoring", "DeviceMonitoring")
12✔
423
        if not isinstance(target, DeviceMonitoring.device.field.related_model):
12✔
424
            return
12✔
425
        try:
12✔
426
            monitoring = target.monitoring
12✔
427
        except DeviceMonitoring.DoesNotExist:
×
428
            monitoring = DeviceMonitoring.objects.create(device=target)
×
429
        monitoring.update_status(monitoring._get_status_from_metrics())
12✔
430

431
    def _get_status_from_metrics(self):
12✔
432
        """Returns the device status from related metric health."""
433
        status = "ok"
12✔
434
        unhealthy_critical_metrics = 0
12✔
435
        for related_metric in self.related_metrics.filter(is_healthy=False):
12✔
436
            status = "problem"
12✔
437
            if self.is_metric_critical(related_metric):
12✔
438
                unhealthy_critical_metrics += 1
12✔
439
        if app_settings.CRITICAL_DEVICE_METRICS and unhealthy_critical_metrics == len(
12✔
440
            app_settings.CRITICAL_DEVICE_METRICS
441
        ):
442
            status = "critical"
12✔
443
        return status
12✔
444

445
    @staticmethod
12✔
446
    def is_metric_critical(metric):
12✔
447
        for critical in app_settings.CRITICAL_DEVICE_METRICS:
12✔
448
            if all(
12✔
449
                [
450
                    metric.key == critical["key"],
451
                    metric.field_name == critical["field_name"],
452
                ]
453
            ):
454
                return True
12✔
455
        return False
12✔
456

457
    @classmethod
12✔
458
    def handle_disabled_organization(cls, organization_id):
12✔
459
        """Handles the disabling of an organization.
460

461
        Clears the management IP of all devices belonging to a disabled
462
        organization and set their monitoring status to 'unknown'.
463

464
        Parameters: - organization_id (int): The ID of the disabled
465
        organization.
466

467
        Returns: - None
468
        """
469
        load_model("config", "Device").objects.filter(
12✔
470
            organization_id=organization_id
471
        ).update(management_ip="")
472
        cls.objects.filter(device__organization_id=organization_id).update(
12✔
473
            status="unknown"
474
        )
475

476
    @classmethod
12✔
477
    def handle_deactivated_device(cls, instance, **kwargs):
12✔
478
        """Handles the deactivation of a device
479

480
        Sets the device's monitoring status to 'deactivated'
481

482
        Parameters: - instance (Device): The device object
483
        which is deactivated
484

485
        Returns: - None
486
        """
487
        cls.objects.filter(device_id=instance.id).update(status="deactivated")
12✔
488

489
    @classmethod
12✔
490
    def handle_activated_device(cls, instance, **kwargs):
12✔
491
        """Handles the activation of a deactivated device
492

493
        Sets the device's monitoring status to 'unknown'
494

495
        Parameters: - instance (Device): The device object
496
        which is deactivated
497

498
        Returns: - None
499
        """
500
        cls.objects.filter(device_id=instance.id).update(status="unknown")
12✔
501

502
    @classmethod
12✔
503
    def _get_critical_metric_keys(cls):
12✔
504
        return [metric["key"] for metric in app_settings.CRITICAL_DEVICE_METRICS]
12✔
505

506
    @classmethod
12✔
507
    def handle_critical_metric(cls, instance, **kwargs):
12✔
508
        critical_metrics = cls._get_critical_metric_keys()
12✔
509
        if instance.check_type in critical_metrics:
12✔
510
            try:
12✔
511
                device_monitoring = cls.objects.get(device=instance.content_object)
12✔
512
                if not instance.is_active or kwargs.get("signal") == post_delete:
12✔
513
                    device_monitoring.update_status("unknown")
12✔
514
            except cls.DoesNotExist:
×
515
                pass
×
516

517

518
class AbstractWifiClient(TimeStampedEditableModel):
12✔
519
    id = None
12✔
520
    mac_address = models.CharField(
12✔
521
        max_length=17,
522
        db_index=True,
523
        primary_key=True,
524
        validators=[mac_address_validator],
525
        help_text=_("MAC address"),
526
    )
527
    vendor = models.CharField(max_length=200, blank=True, null=True)
12✔
528
    he = models.BooleanField(null=True, blank=True, default=None, verbose_name="HE")
12✔
529
    vht = models.BooleanField(null=True, blank=True, default=None, verbose_name="VHT")
12✔
530
    ht = models.BooleanField(null=True, blank=True, default=None, verbose_name="HT")
12✔
531
    wmm = models.BooleanField(default=False, verbose_name="WMM")
12✔
532
    wds = models.BooleanField(default=False, verbose_name="WDS")
12✔
533
    wps = models.BooleanField(default=False, verbose_name="WPS")
12✔
534

535
    class Meta:
12✔
536
        abstract = True
12✔
537
        verbose_name = _("WiFi Client")
12✔
538
        ordering = ("-created",)
12✔
539

540
    @classmethod
12✔
541
    @cache_memoize(CACHE_TIMEOUT)
12✔
542
    def get_wifi_client(cls, mac_address):
12✔
543
        wifi_client, _ = cls.objects.get_or_create(mac_address=mac_address)
12✔
544
        return wifi_client
12✔
545

546
    @classmethod
12✔
547
    def invalidate_cache(cls, instance, *args, **kwargs):
12✔
548
        if kwargs.get("created"):
12✔
549
            return
12✔
550
        cls.get_wifi_client.invalidate(cls, instance.mac_address)
12✔
551

552

553
class AbstractWifiSession(TimeStampedEditableModel):
12✔
554
    created = None
12✔
555

556
    device = models.ForeignKey(
12✔
557
        swapper.get_model_name("config", "Device"),
558
        on_delete=models.CASCADE,
559
    )
560
    wifi_client = models.ForeignKey(
12✔
561
        swapper.get_model_name("device_monitoring", "WifiClient"),
562
        on_delete=models.CASCADE,
563
    )
564
    ssid = models.CharField(
12✔
565
        max_length=32, blank=True, null=True, verbose_name=_("SSID")
566
    )
567
    interface_name = models.CharField(
12✔
568
        max_length=15,
569
    )
570
    start_time = models.DateTimeField(
12✔
571
        verbose_name=_("start time"),
572
        db_index=True,
573
        auto_now=True,
574
    )
575
    stop_time = models.DateTimeField(
12✔
576
        verbose_name=_("stop time"),
577
        db_index=True,
578
        null=True,
579
        blank=True,
580
    )
581

582
    class Meta:
12✔
583
        abstract = True
12✔
584
        verbose_name = _("WiFi Session")
12✔
585
        ordering = ("-start_time",)
12✔
586

587
    def __str__(self):
12✔
588
        return self.mac_address
12✔
589

590
    @property
12✔
591
    def mac_address(self):
12✔
592
        return self.wifi_client.mac_address
12✔
593

594
    @property
12✔
595
    def vendor(self):
12✔
596
        return self.wifi_client.vendor
12✔
597

598
    @classmethod
12✔
599
    def offline_device_close_session(
12✔
600
        cls, metric, tolerance_crossed, first_time, target, **kwargs
601
    ):
602
        if (
12✔
603
            not first_time
604
            and tolerance_crossed
605
            and not metric.is_healthy_tolerant
606
            and AbstractDeviceMonitoring.is_metric_critical(metric)
607
        ):
608
            tasks.offline_device_close_session.delay(device_id=target.pk)
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc