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 7 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
64 changes: 64 additions & 0 deletions care/facility/migrations/0371_assetavailabilityrecord.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 4.2.2 on 2023-07-14 12:32

import uuid

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


class Migration(migrations.Migration):
dependencies = [
("facility", "0370_merge_20230705_1500"),
]

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.IntegerField(
choices=[
(0, "NOT_MONITORED"),
(1, "OPERATIONAL"),
(2, "DOWN"),
(3, "UNDER_MAINTENANCE"),
],
default=0,
),
),
("timestamp", models.DateTimeField()),
(
"asset",
models.ForeignKey(
on_delete=django.db.models.deletion.PROTECT, to="facility.asset"
),
),
],
options={
"ordering": ["-timestamp"],
},
),
]
12 changes: 12 additions & 0 deletions care/facility/migrations/0372_merge_20230714_1912.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 4.2.2 on 2023-07-14 13:42

from django.db import migrations


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

operations = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.2 on 2023-07-17 06:26

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("facility", "0372_merge_20230714_1912"),
]

operations = [
migrations.AlterField(
model_name="assetavailabilityrecord",
name="timestamp",
field=models.DateTimeField(unique=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.2.2 on 2023-07-17 06:53

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("facility", "0373_alter_assetavailabilityrecord_timestamp"),
]

operations = [
migrations.AlterField(
model_name="assetavailabilityrecord",
name="timestamp",
field=models.DateTimeField(),
),
migrations.AlterUniqueTogether(
name="assetavailabilityrecord",
unique_together={("asset", "timestamp")},
),
]
30 changes: 30 additions & 0 deletions care/facility/models/asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from care.facility.models.mixins.permissions.asset import AssetsPermissionMixin
from care.users.models import User, phone_number_regex_11
from care.utils.assetintegration.asset_classes import AssetClasses
from care.utils.assetintegration.asset_statuses import AvailabilityStatus
from care.utils.models.base import BaseModel
from care.utils.models.validators import JSONFieldSchemaValidator

Expand Down Expand Up @@ -105,6 +106,35 @@ 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: IntegerField with choices from AvailabilityStatus enum
- 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.
"""

AvailabilityStatusChoices = [(e.value, e.name) for e in AvailabilityStatus]

asset = models.ForeignKey(Asset, on_delete=models.PROTECT, null=False, blank=False)
status = models.IntegerField(
choices=AvailabilityStatusChoices,
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
default=AvailabilityStatus.NOT_MONITORED.value,
)
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",
)
86 changes: 86 additions & 0 deletions care/facility/tasks/asset_monitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
from datetime import datetime
from typing import Any

from celery import shared_task

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


@shared_task
def check_asset_status():
print("Checking Asset Status", datetime.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,
)
asset_class: BaseAssetIntegration = AssetClasses[asset.asset_class].value(
{
**asset.meta,
"middleware_hostname": hostname,
}
)
result: Any = {}

if hostname in middleware_status_cache:
result = middleware_status_cache[hostname]
else:
try:
result = asset_class.api_get(asset_class.get_url("devices/status"))
middleware_status_cache[hostname] = result
except Exception as e:
print("Error in Asset Status Check", e)
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved
middleware_status_cache[hostname] = None
continue
Ashesh3 marked this conversation as resolved.
Show resolved Hide resolved

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", datetime.now()),
)

except Exception as e:
print("Error in Asset Status Check", e)
8 changes: 8 additions & 0 deletions care/utils/assetintegration/asset_statuses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import enum


class AvailabilityStatus(enum.Enum):
NOT_MONITORED = 0
OPERATIONAL = 1
DOWN = 2
UNDER_MAINTENANCE = 3
sainak marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions care/utils/assetintegration/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ def api_get(self, url, data=None):
headers={"Authorization": (self.auth_header_type + generate_jwt())},
)
try:
response = req.json()
if req.status_code >= 400:
raise APIException(response, req.status_code)
raise APIException(req.text, req.status_code)
response = req.json()
return response
except json.decoder.JSONDecodeError:
return {"error": "Invalid Response"}
2 changes: 2 additions & 0 deletions config/api_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
AmbulanceViewSet,
)
from care.facility.api.viewsets.asset import (
AssetAvailabilityViewSet,
AssetLocationViewSet,
AssetPublicViewSet,
AssetTransactionViewSet,
Expand Down Expand Up @@ -185,6 +186,7 @@

router.register("asset", AssetViewSet)
router.register("asset_transaction", AssetTransactionViewSet)
router.register("asset_availability", AssetAvailabilityViewSet)

patient_nested_router = NestedSimpleRouter(router, r"patient", lookup="patient")
patient_nested_router.register(r"test_sample", PatientSampleViewSet)
Expand Down
Loading
Loading