Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asset Tracking and Monitoring Enhancements #1454

Merged
merged 17 commits into from
Jul 18, 2023
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions care/facility/api/serializers/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from care.facility.api.serializers.facility import FacilityBareMinimumSerializer
from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
Expand Down Expand Up @@ -165,6 +166,15 @@ class Meta:
exclude = ("deleted", "external_id")


class AssetAvailabilitySerializer(ModelSerializer):
id = UUIDField(source="external_id", read_only=True)
asset = AssetBareMinimumSerializer(read_only=True)

class Meta:
model = AssetAvailabilityRecord
exclude = ("deleted", "external_id")


class UserDefaultAssetLocationSerializer(ModelSerializer):
location_object = AssetLocationSerializer(source="location", read_only=True)

Expand Down
13 changes: 13 additions & 0 deletions care/facility/api/viewsets/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from rest_framework.viewsets import GenericViewSet

from care.facility.api.serializers.asset import (
AssetAvailabilitySerializer,
AssetLocationSerializer,
AssetSerializer,
AssetTransactionSerializer,
Expand All @@ -32,6 +33,7 @@
)
from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AssetLocation,
AssetTransaction,
UserDefaultAssetLocation,
Expand Down Expand Up @@ -128,6 +130,17 @@ def retrieve(self, request, *args, **kwargs):
return Response(hit)


class AssetAvailabilityFilter(filters.FilterSet):
external_id = filters.CharFilter(field_name="asset__external_id")


class AssetAvailabilityViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet):
queryset = AssetAvailabilityRecord.objects.all().select_related("asset")
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
serializer_class = AssetAvailabilitySerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_class = AssetAvailabilityFilter


class AssetViewSet(
ListModelMixin,
RetrieveModelMixin,
Expand Down
66 changes: 66 additions & 0 deletions care/facility/migrations/0372_assetavailabilityrecord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by Django 4.2.2 on 2023-07-18 05:00

import uuid

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("facility", "0371_metaicd11diagnosis_chapter_and_more"),
]

operations = [
migrations.CreateModel(
name="AssetAvailabilityRecord",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"external_id",
models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
),
(
"created_date",
models.DateTimeField(auto_now_add=True, db_index=True, null=True),
),
(
"modified_date",
models.DateTimeField(auto_now=True, db_index=True, null=True),
),
("deleted", models.BooleanField(db_index=True, default=False)),
(
"status",
models.CharField(
choices=[
("Not Monitored", "Not Monitored"),
("Operational", "Operational"),
("Down", "Down"),
("Under Maintenance", "Under Maintenance"),
],
default="Not Monitored",
max_length=20,
),
),
("timestamp", models.DateTimeField()),
(
"asset",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="facility.asset"
),
),
],
options={
"ordering": ["-timestamp"],
"unique_together": {("asset", "timestamp")},
},
),
]
35 changes: 35 additions & 0 deletions care/facility/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@ def get_random_asset_id():
return str(uuid.uuid4())


class AvailabilityStatus(models.TextChoices):
NOT_MONITORED = "Not Monitored"
OPERATIONAL = "Operational"
DOWN = "Down"
UNDER_MAINTENANCE = "Under Maintenance"


class AssetLocation(BaseModel, AssetsPermissionMixin):
"""
This model is also used to store rooms that the assets are in, Since these rooms are mapped to
Expand Down Expand Up @@ -105,6 +112,34 @@ def __str__(self):
return self.name


class AssetAvailabilityRecord(BaseModel):
"""
Model to store the availability status of an asset at a particular timestamp.

Fields:
- asset: ForeignKey to Asset model
- status: CharField with choices from AvailabilityStatus
- timestamp: DateTimeField to store the timestamp of the availability record

Note: A pair of asset and timestamp together should be unique, not just the timestamp alone.
"""

asset = models.ForeignKey(Asset, on_delete=models.PROTECT, null=False, blank=False)
status = models.CharField(
choices=AvailabilityStatus.choices,
default=AvailabilityStatus.NOT_MONITORED,
max_length=20,
)
timestamp = models.DateTimeField(null=False, blank=False)

class Meta:
unique_together = (("asset", "timestamp"),)
ordering = ["-timestamp"]

def __str__(self):
return f"{self.asset.name} - {self.status} - {self.timestamp}"


class UserDefaultAssetLocation(BaseModel):
user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False)
location = models.ForeignKey(
Expand Down
14 changes: 10 additions & 4 deletions care/facility/tasks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from celery import current_app
from celery.schedules import crontab

from care.facility.tasks.asset_monitor import check_asset_status
from care.facility.tasks.cleanup import delete_old_notifications
from care.facility.tasks.summarisation import (
summarise_district_patient,
Expand All @@ -19,12 +20,12 @@ def setup_periodic_tasks(sender, **kwargs):
name="delete_old_notifications",
)
sender.add_periodic_task(
crontab(hour="*/4", minute=59),
crontab(hour="*/4", minute="59"),
summarise_triage.s(),
name="summarise_triage",
)
sender.add_periodic_task(
crontab(hour=23, minute=59),
crontab(hour="23", minute="59"),
summarise_tests.s(),
name="summarise_tests",
)
Expand All @@ -34,12 +35,17 @@ def setup_periodic_tasks(sender, **kwargs):
name="summarise_facility_capacity",
)
sender.add_periodic_task(
crontab(hour="*/1", minute=59),
crontab(hour="*/1", minute="59"),
summarise_patient.s(),
name="summarise_patient",
)
sender.add_periodic_task(
crontab(hour="*/1", minute=59),
crontab(hour="*/1", minute="59"),
summarise_district_patient.s(),
name="summarise_district_patient",
)
sender.add_periodic_task(
crontab(minute="*/30"),
check_asset_status.s(),
name="check_asset_status",
)
97 changes: 97 additions & 0 deletions care/facility/tasks/asset_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import logging
from datetime import datetime
from typing import Any

from celery import shared_task
from django.utils import timezone

from care.facility.models.asset import (
Asset,
AssetAvailabilityRecord,
AvailabilityStatus,
)
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.base import BaseAssetIntegration

logger = logging.getLogger(__name__)


@shared_task
def check_asset_status():
print("Checking Asset Status", timezone.now())
logger.info(f"Checking Asset Status: {timezone.now()}")

assets = Asset.objects.all()
middleware_status_cache = {}

for asset in assets:
if not asset.asset_class or not asset.meta.get("local_ip_address", None):
continue
try:
hostname = asset.meta.get(
"middleware_hostname",
asset.current_location.facility.middleware_address,
)
result: Any = {}

if hostname in middleware_status_cache:
result = middleware_status_cache[hostname]
else:
try:
asset_class: BaseAssetIntegration = AssetClasses[
asset.asset_class
].value(
{
**asset.meta,
"middleware_hostname": hostname,
}
)
result = asset_class.api_get(asset_class.get_url("devices/status"))
middleware_status_cache[hostname] = result
except Exception:
logger.exception("Error in Asset Status Check - Fetching Status")
middleware_status_cache[hostname] = None
continue

if not result:
continue

new_status = None
for status_record in result:
if asset.meta.get("local_ip_address") in status_record.get(
"status", {}
):
new_status = status_record["status"][
asset.meta.get("local_ip_address")
]
else:
new_status = "not_monitored"

last_record = (
AssetAvailabilityRecord.objects.filter(asset=asset)
.order_by("-timestamp")
.first()
)

if new_status == "up":
new_status = AvailabilityStatus.OPERATIONAL
elif new_status == "down":
new_status = AvailabilityStatus.DOWN
elif new_status == "maintenance":
new_status = AvailabilityStatus.UNDER_MAINTENANCE
else:
new_status = AvailabilityStatus.NOT_MONITORED

if not last_record or (
datetime.fromisoformat(status_record.get("time"))
> last_record.timestamp
and last_record.status != new_status.value
):
AssetAvailabilityRecord.objects.create(
asset=asset,
status=new_status.value,
timestamp=status_record.get("time", timezone.now()),
)

except Exception:
logger.exception("Error in Asset Status Check")
20 changes: 11 additions & 9 deletions care/facility/tests/test_asset_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@
class AssetViewSetTestCase(TestBase, TestClassMixin, APITestCase):
asset_id = None

def setUp(self):
self.factory = APIRequestFactory()
state = self.create_state()
district = self.create_district(state=state)
self.user = self.create_user(district=district, username="test user")
facility = self.create_facility(district=district, user=self.user)
self.asset1_location = AssetLocation.objects.create(
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.factory = APIRequestFactory()
state = cls.create_state()
district = cls.create_district(state=state)
cls.user = cls.create_user(district=district, username="test user")
facility = cls.create_facility(district=district, user=cls.user)
cls.asset1_location = AssetLocation.objects.create(
name="asset1 location", location_type=1, facility=facility
)
self.asset = Asset.objects.create(
name="Test Asset", current_location=self.asset1_location, asset_type=50
cls.asset = Asset.objects.create(
name="Test Asset", current_location=cls.asset1_location, asset_type=50
)

def test_list_assets(self):
Expand Down
Loading
Loading